diff --git a/frontend/package.json b/frontend/package.json index c4b5b3c95..877357e28 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,9 +5,7 @@ "author": "Kaleidos INC", "private": true, "packageManager": "yarn@4.2.2", - "browserslist": [ - "defaults" - ], + "browserslist": ["defaults"], "type": "module", "repository": { "type": "git", @@ -29,6 +27,7 @@ "token-test:compile": "clojure -M:dev:shadow-cljs compile test-esm --config-merge '{:autorun false}'", "token-test:run": "bun target/tests-esm.cjs", "token-test:watch": "clojure -M:dev:shadow-cljs watch test-esm", + "token-test:nodemon": "nodemon --watch ./target/tests-esm.cjs --exec 'bun run token-test:run'", "token-test": "yarn run token-test:compile && yarn run token-test:run", "translations:validate": "node ./scripts/validate-translations.js", "translations:find-unused": "node ./scripts/find-unused-translations.js", diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index 7201b1dcb..813e319c1 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -163,7 +163,9 @@ :output-to "target/tests-esm.cjs" :output-dir "target/test-esm" :ns-regexp "^token-tests.*-test$" - :autorun true + :autorun false + + :devtools {:http-port 3460} :compiler-options {:output-feature-set :es2020 diff --git a/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md b/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md index 35d40bdfb..5574e332c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md +++ b/frontend/src/app/main/ui/workspace/tokens/CHANGELOG.md @@ -18,6 +18,18 @@ If possible add video here from PR as well ## Changes +### 2024-07-05 - UX Improvements when applying tokens + +[Link to PR](https://github.com/tokens-studio/tokens-studio-for-penpot/compare/token-studio-develop...ux-improvements?body=&expand=1) + +- When unapplying tokens, the shape doesn't change anymore +- Multi Select behavior according to [Specs](https://github.com/tokens-studio/obsidian-docs/blob/31f0d7f98ff5ac922970f3009fe877cc02d6d0cd/Products/TS%20for%20Penpot/Specs/Token%20State%20Specs.md) +- Undo for applying tokens and change the shape is now one undo step + (before applying a token created multiple undo steps) + +[Video](https://github.com/tokens-studio/tokens-studio-for-penpot/assets/1898374/01d9d429-cab1-41cd-a3ff-495003edd3e8 +) + ### 2024-07-01 - Disallow creating tokens at existing paths Disallow creating tokens at an existing path. diff --git a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs index 0b90a0e50..352268480 100644 --- a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs @@ -22,6 +22,7 @@ [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.icons :as i] [app.main.ui.workspace.tokens.core :as wtc] + [app.main.ui.workspace.tokens.token :as wtt] [app.util.dom :as dom] [app.util.timers :as timers] [clojure.set :as set] @@ -212,7 +213,7 @@ (defn additional-actions [{:keys [token-id token-type selected-shapes] :as context-data}] (let [attributes->actions (fn [update-fn coll] (for [{:keys [attributes] :as item} coll] - (let [selected? (wtc/tokens-applied? {:id token-id} selected-shapes attributes)] + (let [selected? (wtt/shapes-token-applied? {:id token-id} selected-shapes attributes)] (assoc item :action #(update-fn context-data attributes) :selected? selected?))))] diff --git a/frontend/src/app/main/ui/workspace/tokens/core.cljs b/frontend/src/app/main/ui/workspace/tokens/core.cljs index 682cefde4..cb03a7a27 100644 --- a/frontend/src/app/main/ui/workspace/tokens/core.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/core.cljs @@ -9,36 +9,25 @@ [app.common.data :as d :refer [ordered-map]] [app.common.types.shape.radius :as ctsr] [app.common.types.token :as ctt] - [app.libs.file-builder :as fb] [app.main.data.tokens :as dt] [app.main.data.workspace :as udw] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.transforms :as dwt] + [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.workspace.tokens.style-dictionary :as sd] + [app.main.ui.workspace.tokens.token :as wtt] [app.util.dom :as dom] [app.util.webapi :as wapi] + [beicon.v2.core :as rx] [cuerdas.core :as str] + [potok.v2.core :as ptk] [promesa.core :as p])) ;; Helpers --------------------------------------------------------------------- -(defn token-applied? - "Test if `token` is applied to a `shape` with the given `token-attributes`." - [token shape token-attributes] - (let [{:keys [id]} token - applied-tokens (get shape :applied-tokens {})] - (some (fn [attr] - (= (get applied-tokens attr) id)) - token-attributes))) - -(defn tokens-applied? - "Test if `token` is applied to to any of `shapes` with the given `token-attributes`." - [token shapes token-attributes] - (some #(token-applied? token % token-attributes) shapes)) - (defn resolve-token-value [{:keys [value resolved-value] :as token}] (or resolved-value @@ -53,38 +42,124 @@ (->> (vals tokens) (group-by :type))) -(defn tokens-name-map - "Convert tokens into a map with their `:name` as the key. - - E.g.: {\"sm\" {:token-type :border-radius :id #uuid \"000\" ...}}" - [tokens] - (->> (map (fn [{:keys [name] :as token}] [name token]) tokens) - (into {}))) - -(defn tokens-name-map-for-type - "Convert tokens with `token-type` into a map with their `:name` as the key. - - E.g.: {\"sm\" {:token-type :border-radius :id #uuid \"000\" ...}}" - [token-type tokens] - (-> (group-tokens-by-type tokens) - (get token-type []) - (tokens-name-map))) - (defn tokens-name-map->select-options [{:keys [shape tokens attributes selected-attributes]}] - (->> (tokens-name-map tokens) + (->> (wtt/token-names-map tokens) (map (fn [[_k {:keys [name] :as item}]] (cond-> (assoc item :label name) - (token-applied? item shape (or selected-attributes attributes)) (assoc :selected? true)))))) + (wtt/token-applied? item shape (or selected-attributes attributes)) (assoc :selected? true)))))) -;; Update functions ------------------------------------------------------------ +;; Shape Update Functions ------------------------------------------------------ + +(defn update-shape-radius [value shape-ids] + (dch/update-shapes shape-ids + (fn [shape] + (when (ctsr/has-radius? shape) + (ctsr/set-radius-1 shape value))) + {:reg-objects? true + :attrs ctt/border-radius-keys})) + +(defn update-shape-dimensions [value shape-ids] + (ptk/reify ::update-shape-dimensions + ptk/WatchEvent + (watch [_ _ _] + (rx/of + (dwt/update-dimensions shape-ids :width value) + (dwt/update-dimensions shape-ids :height value))))) + +(defn update-opacity [value shape-ids] + (dch/update-shapes shape-ids #(assoc % :opacity value))) + +(defn update-stroke-width + [value shape-ids] + (dch/update-shapes shape-ids (fn [shape] + (when (seq (:strokes shape)) + (assoc-in shape [:strokes 0 :stroke-width] value))))) + +(defn update-rotation [value shape-ids] + (ptk/reify ::update-shape-dimensions + ptk/WatchEvent + (watch [_ _ _] + (rx/of + (udw/trigger-bounding-box-cloaking shape-ids) + (udw/increase-rotation shape-ids value))))) + +(defn update-layout-spacing-column [value shape-ids] + (ptk/reify ::update-layout-spacing-column + ptk/WatchEvent + (watch [_ state _] + (rx/concat + (for [shape-id shape-ids] + (let [shape (dt/get-shape-from-state shape-id state) + layout-direction (:layout-flex-dir shape) + layout-update (if (or (= layout-direction :row-reverse) (= layout-direction :row)) + {:layout-gap {:column-gap value}} + {:layout-gap {:row-gap value}})] + (dwsl/update-layout [shape-id] layout-update))))))) + +;; Events ---------------------------------------------------------------------- + +(defn apply-token + [{:keys [attributes shape-ids token on-update-shape] :as _props}] + (ptk/reify ::apply-token + ptk/WatchEvent + (watch [_ state _] + (->> (rx/from (sd/resolve-tokens+ (get-in state [:workspace-data :tokens]))) + (rx/mapcat + (fn [sd-tokens] + (let [undo-id (js/Symbol) + resolved-value (-> (get sd-tokens (:id token)) + (resolve-token-value)) + tokenized-attributes (wtt/attributes-map attributes (:id token))] + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/update-shapes shape-ids (fn [shape] + (update shape :applied-tokens merge tokenized-attributes))) + (when on-update-shape + (on-update-shape resolved-value shape-ids attributes)) + (dwu/commit-undo-transaction undo-id))))))))) + +(defn unapply-token + "Removes `attributes` that match `token` for `shape-ids`. + + Doesn't update shape attributes." + [{:keys [attributes token shape-ids] :as _props}] + (ptk/reify ::unapply-token + ptk/WatchEvent + (watch [_ _ _] + (rx/of + (let [remove-token #(when % (wtt/remove-attributes-for-token-id attributes (:id token) %))] + (dch/update-shapes + shape-ids + (fn [shape] + (update shape :applied-tokens remove-token)))))))) + +(defn toggle-token + [{:keys [token-type-props token shapes] :as _props}] + (ptk/reify ::on-toggle-token + ptk/WatchEvent + (watch [_ _ _] + (let [{:keys [attributes on-update-shape]} token-type-props + unapply-tokens? (wtt/shapes-token-applied? token shapes (:attributes token-type-props)) + shape-ids (map :id shapes)] + (if unapply-tokens? + (rx/of + (unapply-token {:attributes attributes + :token token + :shape-ids shape-ids})) + (rx/of + (apply-token {:attributes attributes + :token token + :shape-ids shape-ids + :on-update-shape on-update-shape}))))))) (defn on-apply-token [{:keys [token token-type-props selected-shapes] :as _props}] (let [{:keys [attributes on-apply on-update-shape] :or {on-apply dt/update-token-from-attributes}} token-type-props shape-ids (->> selected-shapes (eduction - (remove #(tokens-applied? token % attributes)) + (remove #(wtt/shapes-token-applied? token % attributes)) (map :id)))] + (p/let [sd-tokens (sd/resolve-workspace-tokens+ {:debug? true})] (let [resolved-token (get sd-tokens (:id token)) resolved-token-value (resolve-token-value resolved-token)] @@ -94,45 +169,6 @@ :attributes attributes})) (on-update-shape resolved-token-value shape-ids attributes)))))) -(defn update-shape-radius [value shape-ids] - (st/emit! - (dch/update-shapes shape-ids - (fn [shape] - (when (ctsr/has-radius? shape) - (ctsr/set-radius-1 shape value))) - {:reg-objects? true - :attrs ctt/border-radius-keys}))) - -(defn update-shape-dimensions [value shape-ids] - (st/emit! - (dwt/update-dimensions shape-ids :width value) - (dwt/update-dimensions shape-ids :height value))) - -(defn update-opacity [value shape-ids] - (st/emit! - (dch/update-shapes shape-ids #(assoc % :opacity value)))) - -(defn update-stroke-width - [value shape-ids] - (st/emit! - (dch/update-shapes shape-ids (fn [shape] - (when (seq (:strokes shape)) - (assoc-in shape [:strokes 0 :stroke-width] value)))))) - -(defn update-rotation [value shape-ids] - (st/emit! (udw/trigger-bounding-box-cloaking shape-ids) - (udw/increase-rotation shape-ids value))) - -(defn update-layout-spacing-column [value shape-ids] - (doseq [shape-id shape-ids] - (let [shape (dt/get-shape-from-state shape-id @st/state) - layout-direction (:layout-flex-dir shape) - layout-update (if (or (= layout-direction :row-reverse) (= layout-direction :row)) - {:layout-gap {:column-gap value}} - {:layout-gap {:row-gap value}})] - (st/emit! - (dwsl/update-layout [shape-id] layout-update))))) - ;; JSON export functions ------------------------------------------------------- (defn encode-tokens diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 3ab3519e0..4edabdfcc 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -16,6 +16,7 @@ [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.main.ui.workspace.tokens.core :as wtc] [app.main.ui.workspace.tokens.style-dictionary :as sd] + [app.main.ui.workspace.tokens.token :as wtt] [app.util.dom :as dom] [cuerdas.core :as str] [okulary.core :as l] @@ -98,9 +99,10 @@ (mf/deps selected-shapes token-type-props) (fn [event token] (dom/stop-propagation event) - (wtc/on-apply-token {:token token - :token-type-props token-type-props - :selected-shapes selected-shapes}))) + (st/emit! + (wtc/toggle-token {:token token + :shapes selected-shapes + :token-type-props token-type-props})))) tokens-count (count tokens)] [:div {:on-click on-toggle-open-click} [:& cmm/asset-section {:icon (mf/fnc icon-wrapper [_] @@ -121,7 +123,7 @@ [:& token-pill {:key (:id token) :token token - :highlighted? (wtc/tokens-applied? token selected-shapes attributes) + :highlighted? (wtt/shapes-token-applied? token selected-shapes attributes) :on-click #(on-token-pill-click % token) :on-context-menu #(on-context-menu % token)}])]])]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs index 73e68ad4c..911fbafe9 100644 --- a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs @@ -53,7 +53,7 @@ "Resolves references and math expressions using StyleDictionary. Returns a promise with the resolved dictionary." [tokens & {:keys [debug?] :as config}] - (let [performance-start (js/window.performance.now) + (let [performance-start (js/performance.now) sd (tokens->style-dictionary+ tokens config)] (when debug? (js/console.log "StyleDictionary" sd)) @@ -61,7 +61,7 @@ (.buildAllPlatforms "json") (.catch js/console.error) (.then (fn [^js resp] - (let [performance-end (js/window.performance.now) + (let [performance-end (js/performance.now) duration-ms (- performance-end performance-start) resolved-tokens (.-allTokens resp)] (when debug? diff --git a/frontend/src/app/main/ui/workspace/tokens/token.cljs b/frontend/src/app/main/ui/workspace/tokens/token.cljs index ce288113d..79fbb0e2d 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token.cljs @@ -2,6 +2,36 @@ (:require [cuerdas.core :as str])) +(defn attributes-map + "Creats an attributes map using collection of `attributes` for `id`." + [attributes id] + (->> (map (fn [attr] {attr id}) attributes) + (into {}))) + +(defn remove-attributes-for-token-id + "Removes applied tokens with `token-id` for the given `attributes` set from `applied-tokens`." + [attributes token-id applied-tokens] + (let [attr? (set attributes)] + (->> (remove (fn [[k v]] + (and (attr? k) + (= v token-id))) + applied-tokens) + (into {})))) + +(defn token-applied? + "Test if `token` is applied to a `shape` with the given `token-attributes`." + [token shape token-attributes] + (let [{:keys [id]} token + applied-tokens (get shape :applied-tokens {})] + (some (fn [attr] + (= (get applied-tokens attr) id)) + token-attributes))) + +(defn shapes-token-applied? + "Test if `token` is applied to to any of `shapes` with the given `token-attributes`." + [token shapes token-attributes] + (some #(token-applied? token % token-attributes) shapes)) + (defn token-name->path "Splits token-name into a path vector split by `.` characters. @@ -21,6 +51,14 @@ {:path (seq path) :selector selector})) +(defn token-names-map + "Convert tokens into a map with their `:name` as the key. + + E.g.: {\"sm\" {:token-type :border-radius :id #uuid \"000\" ...}}" + [tokens] + (->> (map (fn [{:keys [name] :as token}] [name token]) tokens) + (into {}))) + (defn token-names-tree "Convert tokens into a nested tree with their `:name` as the path." [tokens] diff --git a/frontend/test/token_tests/helpers/state.cljs b/frontend/test/token_tests/helpers/state.cljs new file mode 100644 index 000000000..6b9368210 --- /dev/null +++ b/frontend/test/token_tests/helpers/state.cljs @@ -0,0 +1,49 @@ +(ns token-tests.helpers.state + (:require + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) + +(defn stop-on + "Helper function to be used with async version of run-store. + + Will stop the execution after event with `event-type` has completed." + [event-type] + (fn [stream] + (->> stream + #_(rx/tap #(prn (ptk/type %))) + (rx/filter #(ptk/type? event-type %))))) + +(def stop-on-send-update-indices + "Stops on `send-update-indices` function being called, which should be the last function of an event chain." + (stop-on :app.main.data.workspace.changes/send-update-indices)) + +;; Support for async events in tests +;; https://chat.kaleidos.net/penpot-partners/pl/tz1yoes3w3fr9qanxqpuhoz3ch +(defn run-store + "Async version of `frontend-tests.helpers.state/run-store`." + ([store done events completed-cb] + (run-store store done events completed-cb nil)) + ([store done events completed-cb stopper] + (let [stream (ptk/input-stream store)] + (->> stream + (rx/take-until (if stopper + (stopper stream) + (rx/filter #(= :the/end %) stream))) + (rx/last) + (rx/tap (fn [] + (completed-cb @store))) + (rx/subs! (fn [_] (done)) + (fn [cause] + (js/console.log "[error]:" cause)) + (fn [_] + #_(js/console.log "[complete]")))) + (doall (for [event events] + (ptk/emit! store event))) + (ptk/emit! store :the/end)))) + +(defn run-store-async + "Helper version of `run-store` that automatically stops on the `send-update-indices` event" + ([store done events completed-cb] + (run-store store done events completed-cb stop-on-send-update-indices)) + ([store done events completed-cb stop-on] + (run-store store done events completed-cb stop-on))) diff --git a/frontend/test/token_tests/helpers/tokens.cljs b/frontend/test/token_tests/helpers/tokens.cljs new file mode 100644 index 000000000..716dd0755 --- /dev/null +++ b/frontend/test/token_tests/helpers/tokens.cljs @@ -0,0 +1,24 @@ +(ns token-tests.helpers.tokens + (:require + [app.common.test-helpers.ids-map :as thi] + [app.main.ui.workspace.tokens.token :as wtt])) + +(defn add-token [state label params] + (let [id (thi/new-id! label) + token (assoc params :id id)] + (update-in state [:data :tokens] assoc id token))) + +(defn get-token [file label] + (let [id (thi/id label)] + (get-in file [:data :tokens id]))) + +(defn apply-token-to-shape [file shape-label token-label attributes] + (let [first-page-id (get-in file [:data :pages 0]) + shape-id (thi/id shape-label) + token-id (thi/id token-label) + applied-attributes (wtt/attributes-map attributes token-id)] + (update-in file [:data + :pages-index first-page-id + :objects shape-id + :applied-tokens] + merge applied-attributes))) diff --git a/frontend/test/token_tests/logic/token_actions_test.cljs b/frontend/test/token_tests/logic/token_actions_test.cljs new file mode 100644 index 000000000..3fadf0cdd --- /dev/null +++ b/frontend/test/token_tests/logic/token_actions_test.cljs @@ -0,0 +1,274 @@ +(ns token-tests.logic.token-actions-test + (:require + [app.common.test-helpers.compositions :as ctho] + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.shapes :as cths] + [app.main.ui.workspace.tokens.core :as wtc] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.pages :as thp] + [frontend-tests.helpers.state :as ths] + [token-tests.helpers.state :as tohs] + [token-tests.helpers.tokens :as toht])) + +(t/use-fixtures :each + {:before thp/reset-idmap!}) + +(defn- setup-file + [] + (-> (cthf/sample-file :file-1 :page-label :page-1) + (ctho/add-rect :rect-1 {}) + (ctho/add-rect :rect-2 {}) + (ctho/add-rect :rect-3 {}) + (toht/add-token :token-1 {:value "12" + :name "borderRadius.sm" + :type :border-radius}) + (toht/add-token :token-2 {:value "{borderRadius.sm} * 2" + :name "borderRadius.md" + :type :border-radius}))) + +(t/deftest test-apply-token + (t/testing "applying a token twice with the same attributes will override") + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtc/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:rx :ry} + :token (toht/get-token file :token-1) + :on-update-shape wtc/update-shape-radius}) + (wtc/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:rx :ry} + :token (toht/get-token file :token-2) + :on-update-shape wtc/update-shape-radius})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-2' (toht/get-token file' :token-2) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:rx (:applied-tokens rect-1')) (:id token-2'))) + (t/is (= (:ry (:applied-tokens rect-1')) (:id token-2'))) + (t/is (= (:rx rect-1') 24)) + (t/is (= (:ry rect-1') 24)))))))) + +(t/deftest test-apply-border-radius + (t/testing "applies radius token and updates the shapes radius") + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtc/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:rx :ry} + :token (toht/get-token file :token-2) + :on-update-shape wtc/update-shape-radius})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-2' (toht/get-token file' :token-2) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:rx (:applied-tokens rect-1')) (:id token-2'))) + (t/is (= (:ry (:applied-tokens rect-1')) (:id token-2'))) + (t/is (= (:rx rect-1') 24)) + (t/is (= (:ry rect-1') 24)))))))) + +(t/deftest test-apply-dimensions + (t/testing "applies dimensions token and updates the shapes width and height") + (t/async + done + (let [file (-> (setup-file) + (toht/add-token :token-target {:value "100" + :name "dimensions.sm" + :type :dimensions})) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtc/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:width :height} + :token (toht/get-token file :token-target) + :on-update-shape wtc/update-shape-dimensions})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-target' (toht/get-token file' :token-target) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:width (:applied-tokens rect-1')) (:id token-target'))) + (t/is (= (:height (:applied-tokens rect-1')) (:id token-target'))) + (t/is (= (:width rect-1') 100)) + (t/is (= (:height rect-1') 100)))))))) + +(t/deftest test-apply-sizing + (t/testing "applies sizing token and updates the shapes width and height") + (t/async + done + (let [file (-> (setup-file) + (toht/add-token :token-target {:value "100" + :name "sizing.sm" + :type :sizing})) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtc/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:width :height} + :token (toht/get-token file :token-target) + :on-update-shape wtc/update-shape-dimensions})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-target' (toht/get-token file' :token-target) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:width (:applied-tokens rect-1')) (:id token-target'))) + (t/is (= (:height (:applied-tokens rect-1')) (:id token-target'))) + (t/is (= (:width rect-1') 100)) + (t/is (= (:height rect-1') 100)))))))) + +(t/deftest test-apply-opacity + (t/testing "applies opacity token and updates the shapes opacity") + (t/async + done + (let [file (-> (setup-file) + (toht/add-token :token-target {:value "0.5" + :name "opacity.medium" + :type :opacity})) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtc/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:opacity} + :token (toht/get-token file :token-target) + :on-update-shape wtc/update-opacity})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-target' (toht/get-token file' :token-target) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:opacity (:applied-tokens rect-1')) (:id token-target'))) + ;; TODO Fix opacity shape update not working? + #_(t/is (= (:opacity rect-1') 0.5)))))))) + +(t/deftest test-apply-rotation + (t/testing "applies rotation token and updates the shapes rotation") + (t/async + done + (let [file (-> (setup-file) + (toht/add-token :token-target {:value "120" + :name "rotation.medium" + :type :rotation})) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + events [(wtc/apply-token {:shape-ids [(:id rect-1)] + :attributes #{:rotation} + :token (toht/get-token file :token-target) + :on-update-shape wtc/update-rotation})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-target' (toht/get-token file' :token-target) + rect-1' (cths/get-shape file' :rect-1)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (= (:rotation (:applied-tokens rect-1')) (:id token-target'))) + (t/is (= (:rotation rect-1') 120)))))))) + +(t/deftest test-toggle-token-none + (t/testing "should apply token to all selected items, where no item has the token applied" + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + rect-1 (cths/get-shape file :rect-1) + rect-2 (cths/get-shape file :rect-2) + events [(wtc/toggle-token {:shapes [rect-1 rect-2] + :token-type-props {:attributes #{:rx :ry} + :on-update-shape wtc/update-shape-radius} + :token (toht/get-token file :token-2)})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + token-2' (toht/get-token file' :token-2) + rect-1' (cths/get-shape file' :rect-1) + rect-2' (cths/get-shape file' :rect-2)] + (t/is (some? (:applied-tokens rect-1'))) + (t/is (some? (:applied-tokens rect-2'))) + (t/is (= (:rx (:applied-tokens rect-1')) (:id token-2'))) + (t/is (= (:rx (:applied-tokens rect-2')) (:id token-2'))) + (t/is (= (:ry (:applied-tokens rect-1')) (:id token-2'))) + (t/is (= (:ry (:applied-tokens rect-2')) (:id token-2'))) + (t/is (= (:rx rect-1') 24)) + (t/is (= (:rx rect-2') 24))))))))) + +(t/deftest test-toggle-token-mixed + (t/testing "should unapply given token if one of the selected items has the token applied while keeping other tokens with some attributes" + (t/async + done + (let [file (-> (setup-file) + (toht/apply-token-to-shape :rect-1 :token-1 #{:rx :ry}) + (toht/apply-token-to-shape :rect-3 :token-2 #{:rx :ry})) + store (ths/setup-store file) + + rect-with-token (cths/get-shape file :rect-1) + rect-without-token (cths/get-shape file :rect-2) + rect-with-other-token (cths/get-shape file :rect-3) + + events [(wtc/toggle-token {:shapes [rect-with-token rect-without-token rect-with-other-token] + :token (toht/get-token file :token-1) + :token-type-props {:attributes #{:rx :ry}}})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + rect-with-token' (cths/get-shape file' :rect-1) + rect-without-token' (cths/get-shape file' :rect-2) + rect-with-other-token' (cths/get-shape file' :rect-3)] + + (t/testing "rect-with-token got the token remove" + (t/is (nil? (:rx (:applied-tokens rect-with-token')))) + (t/is (nil? (:ry (:applied-tokens rect-with-token'))))) + + (t/testing "rect-without-token didn't get updated" + (t/is (= (:applied-tokens rect-without-token') (:applied-tokens rect-without-token)))) + + (t/testing "rect-with-other-token didn't get updated" + (t/is (= (:applied-tokens rect-with-other-token') (:applied-tokens rect-with-other-token))))))))))) + +(t/deftest test-toggle-token-apply-to-all + (t/testing "should apply token to all if none of the shapes has it applied" + (t/async + done + (let [file (-> (setup-file) + (toht/apply-token-to-shape :rect-1 :token-2 #{:rx :ry}) + (toht/apply-token-to-shape :rect-3 :token-2 #{:rx :ry})) + store (ths/setup-store file) + + rect-with-other-token-1 (cths/get-shape file :rect-1) + rect-without-token (cths/get-shape file :rect-2) + rect-with-other-token-2 (cths/get-shape file :rect-3) + + events [(wtc/toggle-token {:shapes [rect-with-other-token-1 rect-without-token rect-with-other-token-2] + :token (toht/get-token file :token-1) + :token-type-props {:attributes #{:rx :ry}}})]] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-store new-state) + target-token (toht/get-token file' :token-1) + rect-with-other-token-1' (cths/get-shape file' :rect-1) + rect-without-token' (cths/get-shape file' :rect-2) + rect-with-other-token-2' (cths/get-shape file' :rect-3)] + + (t/testing "token got applied to all shapes" + (t/is (= (:rx (:applied-tokens rect-with-other-token-1')) (:id target-token))) + (t/is (= (:rx (:applied-tokens rect-without-token')) (:id target-token))) + (t/is (= (:rx (:applied-tokens rect-with-other-token-2')) (:id target-token))) + + (t/is (= (:ry (:applied-tokens rect-with-other-token-1')) (:id target-token))) + (t/is (= (:ry (:applied-tokens rect-without-token')) (:id target-token))) + (t/is (= (:ry (:applied-tokens rect-with-other-token-2')) (:id target-token))))))))))) diff --git a/frontend/test/token_tests/token_test.cljs b/frontend/test/token_tests/token_test.cljs index 618dc9a21..255adf28e 100644 --- a/frontend/test/token_tests/token_test.cljs +++ b/frontend/test/token_tests/token_test.cljs @@ -9,11 +9,47 @@ [app.main.ui.workspace.tokens.token :as wtt] [cljs.test :as t :include-macros true])) +(t/deftest remove-attributes-for-token-id + (t/testing "removes attributes matching the `token-id`, keeps other attributes" + (t/is (= {:ry :b} + (wtt/remove-attributes-for-token-id + #{:rx :ry} :a {:rx :a :ry :b}))))) + +(t/deftest token-applied-test + (t/testing "matches passed token with `:token-attributes`" + (t/is (true? (wtt/token-applied? {:id :a} {:applied-tokens {:x :a}} #{:x})))) + (t/testing "doesn't match empty token" + (t/is (nil? (wtt/token-applied? {} {:applied-tokens {:x :a}} #{:x})))) + (t/testing "does't match passed token `:id`" + (t/is (nil? (wtt/token-applied? {:id :b} {:applied-tokens {:x :a}} #{:x})))) + (t/testing "doesn't match passed `:token-attributes`" + (t/is (nil? (wtt/token-applied? {:id :a} {:applied-tokens {:x :a}} #{:y}))))) + +(t/deftest tokens-applied-test + (t/testing "is true when single shape matches the token and attributes" + (t/is (true? (wtt/shapes-token-applied? {:id :a} [{:applied-tokens {:x :a}} + {:applied-tokens {:x :b}}] + #{:x})))) + (t/testing "is false when no shape matches the token or attributes" + (t/is (nil? (wtt/shapes-token-applied? {:id :a} [{:applied-tokens {:x :b}} + {:applied-tokens {:x :b}}] + #{:x}))) + (t/is (nil? (wtt/shapes-token-applied? {:id :a} [{:applied-tokens {:x :a}} + {:applied-tokens {:x :a}}] + #{:y}))))) + (t/deftest name->path-test (t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo.bar.baz"))) (t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz"))) (t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz....")))) +(t/deftest tokens-name-map-test + (t/testing "creates a a names map from tokens" + (t/is (= {"border-radius.sm" {:name "border-radius.sm", :value "10"} + "border-radius.md" {:name "border-radius.md", :value "20"}} + (wtt/token-names-map [{:name "border-radius.sm" :value "10"} + {:name "border-radius.md" :value "20"}]))))) + (t/deftest tokens-name-tree-test (t/is (= {"foo" {"bar"