mirror of
https://github.com/penpot/penpot.git
synced 2026-09-22 20:06:16 +00:00
* ✨ Auto link tokens when adding external libraries (provisional) * 🔧 Refactor tokens-lib initialization * 🔧 Add separated TokenStatus to store status apart of TokensLib * 🔧 Make all status operations use the new data structure * 🔧 Normalize status helper functions and access token sets by id * 🔧 Rename :tokens-file to :tokens-source * 🎉 Allow the user to choose the tokens-source of a file * 🎉 Make tokens library readonly when it's in an external file * 🎉 Show tokens in library summaries * 🎉 Show source info in sidebar * 🔧 Fix integration tests * 🐛 Propagate changes of token values in external library * 🎉 Layout updates * 🔧 Refactor tokens source calculations * 🔧 Add harder checks for nil or empty values in everything * 🐛 Fix some integration tests * 🔧 Add integration tests for tokens in external libs * 🔧 Validate and repair missing tokens status * 🎉 Make ui changes optional with config flag * 🐛 Propagate tokens after synchronizing components in ext library * 🐛 Propagate tokens after creating new instances * 🐛 Propagate tokens after synchronizing tokens in ext library * 🐛 Add a tokens source icon to libraries section (#11439) * 🐛 Add a tokens source icon to libraries section * 🐛 Fix ellipsis on library names * ♻️ Remove code under flag on legacy component * 🐛 Fix token theme name on inspect tab * 🎉 Add changes notification (#11476) * 🎉 Add changes notification * ♻️ Change fn names * 🐛 Fix tokens source label truncation and missing translations (#11533) * 🐛 Fix tokens source label truncation and missing translations The tokens source file name always showed, even for the current file, and long names wrapped onto a second line instead of truncating because the header used flex-wrap and overflow-wrap: break-word instead of single-line ellipsis. Show the source row unconditionally (it now displays "This file" when the source is the current file, matching the connected-library case), truncate the file name to one line with an ellipsis, and only attach a tooltip with the full name when the text is actually truncated. Replace the hardcoded UI strings with translated ones and add their English and Spanish entries. AI-assisted-by: claude-sonnet-5 * 🐛 Remove redundant effect dependency in tokens source file-name-truncated? was listed as a dependency of the with-effect that checks and observes label truncation, even though it isn't read inside the effect body. Since the effect itself flips that state via check-file-name-truncated, including it as a dependency caused the ResizeObserver to be needlessly disconnected and reconnected on every truncation change. AI-assisted-by: claude-sonnet-5 * 🐛 Fix small visual error * 🐛 Fix problem with plugins * 🐛 Fix playwright tests --------- Co-authored-by: Eva Marco <evamarcod@gmail.com> Co-authored-by: Eva Marco <eva.marco@kaleidos.net> Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
402 lines
25 KiB
Clojure
402 lines
25 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
|
|
|
(ns common-tests.logic.token-apply-test
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.files.changes-builder :as pcb]
|
|
[app.common.logic.shapes :as cls]
|
|
[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.tokens :as tht]
|
|
[app.common.types.container :as ctn]
|
|
[app.common.types.text :as txt]
|
|
[app.common.types.token :as cto]
|
|
[app.common.types.tokens-lib :as ctob]
|
|
[app.common.types.tokens-status :as ctos]
|
|
[clojure.test :as t]))
|
|
|
|
(t/use-fixtures :each thi/test-fixture)
|
|
|
|
(defn- setup-file
|
|
[]
|
|
(-> (tht/sample-file-with-tokens
|
|
:lib-fn #(-> %
|
|
(ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set)
|
|
:name "test-token-set"))
|
|
(ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :test-theme)
|
|
:name "test-theme"
|
|
:sets #{"test-token-set"}))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-radius)
|
|
:name "token-radius"
|
|
:type :border-radius
|
|
:value 10))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-rotation)
|
|
:name "token-rotation"
|
|
:type :rotation
|
|
:value 30))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-opacity)
|
|
:name "token-opacity"
|
|
:type :opacity
|
|
:value 0.7))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-stroke-width)
|
|
:name "token-stroke-width"
|
|
:type :stroke-width
|
|
:value 2))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-color)
|
|
:name "token-color"
|
|
:type :color
|
|
:value "#00ff00"))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-dimensions)
|
|
:name "token-dimensions"
|
|
:type :dimensions
|
|
:value 100))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-font-size)
|
|
:name "token-font-size"
|
|
:type :font-size
|
|
:value 24))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-letter-spacing)
|
|
:name "token-letter-spacing"
|
|
:type :letter-spacing
|
|
:value 2))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-font-family)
|
|
:name "token-font-family"
|
|
:type :font-family
|
|
:value ["Helvetica" "Arial" "sans-serif"]))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-sizing)
|
|
:name "token-sizing"
|
|
:type :sizing
|
|
:value 10))
|
|
(ctob/add-token (thi/id :test-token-set)
|
|
(ctob/make-token :id (thi/new-id! :token-spacing)
|
|
:name "token-spacing"
|
|
:type :spacing
|
|
:value 30)))
|
|
:status-fn #(-> %
|
|
(ctos/set-tokens-status #{(thi/id :test-theme)}
|
|
#{(thi/id :test-token-set)})))
|
|
(tho/add-frame :frame1
|
|
:layout :flex ;; TODO: those values come from main.data.workspace.shape_layout/default-layout-params
|
|
:layout-flex-dir :row ;; it should be good to use it directly, but first it should be moved to common.logic
|
|
:layout-gap-type :multiple
|
|
:layout-gap {:row-gap 0 :column-gap 0}
|
|
:layout-align-items :start
|
|
:layout-justify-content :start
|
|
:layout-align-content :stretch
|
|
:layout-wrap-type :nowrap
|
|
:layout-padding-type :simple
|
|
:layout-padding {:p1 0 :p2 0 :p3 0 :p4 0})
|
|
(ths/add-sample-shape :circle1 :parent-label :frame-1)
|
|
(tho/add-text :text1 "Hello World!")))
|
|
|
|
(defn- apply-all-tokens
|
|
[file]
|
|
(-> file
|
|
(tht/apply-token-to-shape :frame1 "token-radius" [:r1 :r2 :r3 :r4] [:r1 :r2 :r3 :r4] 10)
|
|
(tht/apply-token-to-shape :frame1 "token-rotation" [:rotation] [:rotation] 30)
|
|
(tht/apply-token-to-shape :frame1 "token-opacity" [:opacity] [:opacity] 0.7)
|
|
(tht/apply-token-to-shape :frame1 "token-stroke-width" [:stroke-width] [:stroke-width] 2)
|
|
(tht/apply-token-to-shape :frame1 "token-color" [:stroke-color] [:stroke-color] "#00ff00")
|
|
(tht/apply-token-to-shape :frame1 "token-color" [:fill] [:fill] "#00ff00")
|
|
(tht/apply-token-to-shape :frame1 "token-dimensions" [:width :height] [:width :height] 100)
|
|
(tht/apply-token-to-shape :text1 "token-font-size" [:font-size] [:font-size] "24")
|
|
(tht/apply-token-to-shape :text1 "token-letter-spacing" [:letter-spacing] [:letter-spacing] "2")
|
|
(tht/apply-token-to-shape :text1 "token-font-family" [:font-family] [:font-family] ["Helvetica" "Arial" "sans-serif"])
|
|
(tht/apply-token-to-shape :circle1
|
|
"token-sizing"
|
|
[:layout-item-max-h :layout-item-max-w :layout-item-min-h :layout-item-min-w]
|
|
[:layout-item-max-h :layout-item-max-w :layout-item-min-h :layout-item-min-w]
|
|
10)
|
|
(tht/apply-token-to-shape :circle1
|
|
"token-spacing"
|
|
[:m1 :m2 :m3 :m4]
|
|
[:layout-item-margin]
|
|
{:m1 30 :m2 30 :m3 30 :m4 30})))
|
|
|
|
(t/deftest apply-tokens-to-shape
|
|
(let [;; ==== Setup
|
|
file (setup-file)
|
|
page (thf/current-page file)
|
|
frame1 (ths/get-shape file :frame1)
|
|
text1 (ths/get-shape file :text1)
|
|
circle1 (ths/get-shape file :circle1)
|
|
token-radius (tht/get-token file (thi/id :test-token-set) (thi/id :token-radius))
|
|
token-rotation (tht/get-token file (thi/id :test-token-set) (thi/id :token-rotation))
|
|
token-opacity (tht/get-token file (thi/id :test-token-set) (thi/id :token-opacity))
|
|
token-stroke-width (tht/get-token file (thi/id :test-token-set) (thi/id :token-stroke-width))
|
|
token-color (tht/get-token file (thi/id :test-token-set) (thi/id :token-color))
|
|
token-dimensions (tht/get-token file (thi/id :test-token-set) (thi/id :token-dimensions))
|
|
token-font-size (tht/get-token file (thi/id :test-token-set) (thi/id :token-font-size))
|
|
token-letter-spacing (tht/get-token file (thi/id :test-token-set) (thi/id :token-letter-spacing))
|
|
token-font-family (tht/get-token file (thi/id :test-token-set) (thi/id :token-font-family))
|
|
token-sizing (tht/get-token file (thi/id :test-token-set) (thi/id :token-sizing))
|
|
token-spacing (tht/get-token file (thi/id :test-token-set) (thi/id :token-spacing))
|
|
|
|
;; ==== Action
|
|
changes (-> (-> (pcb/empty-changes nil)
|
|
(pcb/with-page page)
|
|
(pcb/with-objects (:objects page)))
|
|
(cls/generate-update-shapes [(:id frame1)]
|
|
(fn [shape]
|
|
(as-> shape $
|
|
(cto/apply-token-to-shape {:token token-radius
|
|
:shape $
|
|
:attributes [:r1 :r2 :r3 :r4]})
|
|
(cto/apply-token-to-shape {:token token-rotation
|
|
:shape $
|
|
:attributes [:rotation]})
|
|
(cto/apply-token-to-shape {:token token-opacity
|
|
:shape $
|
|
:attributes [:opacity]})
|
|
(cto/apply-token-to-shape {:token token-stroke-width
|
|
:shape $
|
|
:attributes [:stroke-width]})
|
|
(cto/apply-token-to-shape {:token token-color
|
|
:shape $
|
|
:attributes [:stroke-color]})
|
|
(cto/apply-token-to-shape {:token token-color
|
|
:shape $
|
|
:attributes [:fill]})
|
|
(cto/apply-token-to-shape {:token token-dimensions
|
|
:shape $
|
|
:attributes [:width :height]})))
|
|
(:objects page)
|
|
{})
|
|
(cls/generate-update-shapes [(:id text1)]
|
|
(fn [shape]
|
|
(as-> shape $
|
|
(cto/apply-token-to-shape {:token token-font-size
|
|
:shape $
|
|
:attributes [:font-size]})
|
|
(cto/apply-token-to-shape {:token token-letter-spacing
|
|
:shape $
|
|
:attributes [:letter-spacing]})
|
|
(cto/apply-token-to-shape {:token token-font-family
|
|
:shape $
|
|
:attributes [:font-family]})))
|
|
(:objects page)
|
|
{})
|
|
(cls/generate-update-shapes [(:id circle1)]
|
|
(fn [shape]
|
|
(as-> shape $
|
|
(cto/apply-token-to-shape {:token token-sizing
|
|
:shape $
|
|
:attributes [:layout-item-max-h :layout-item-max-w :layout-item-min-h :layout-item-min-w]})
|
|
(cto/apply-token-to-shape {:token token-spacing
|
|
:shape $
|
|
:attributes [:m1 :m2 :m3 :m4]})))
|
|
(:objects page)
|
|
{}))
|
|
|
|
file' (thf/apply-changes file changes)
|
|
|
|
;; ==== Get
|
|
frame1' (ths/get-shape file' :frame1)
|
|
frame1'-applied-tokens (:applied-tokens frame1')
|
|
text1' (ths/get-shape file' :text1)
|
|
text1'-applied-tokens (:applied-tokens text1')
|
|
circle1' (ths/get-shape file' :circle1)
|
|
circle1'-applied-tokens (:applied-tokens circle1')]
|
|
|
|
;; ==== Check
|
|
(t/is (= (count frame1'-applied-tokens) 11))
|
|
(t/is (= (:r1 frame1'-applied-tokens) "token-radius"))
|
|
(t/is (= (:r2 frame1'-applied-tokens) "token-radius"))
|
|
(t/is (= (:r3 frame1'-applied-tokens) "token-radius"))
|
|
(t/is (= (:r4 frame1'-applied-tokens) "token-radius"))
|
|
(t/is (= (:rotation frame1'-applied-tokens) "token-rotation"))
|
|
(t/is (= (:opacity frame1'-applied-tokens) "token-opacity"))
|
|
(t/is (= (:stroke-width frame1'-applied-tokens) "token-stroke-width"))
|
|
(t/is (= (:stroke-color frame1'-applied-tokens) "token-color"))
|
|
(t/is (= (:fill frame1'-applied-tokens) "token-color"))
|
|
(t/is (= (:width frame1'-applied-tokens) "token-dimensions"))
|
|
(t/is (= (:height frame1'-applied-tokens) "token-dimensions"))
|
|
|
|
(t/is (= (count text1'-applied-tokens) 3))
|
|
(t/is (= (:font-size text1'-applied-tokens) "token-font-size"))
|
|
(t/is (= (:letter-spacing text1'-applied-tokens) "token-letter-spacing"))
|
|
(t/is (= (:font-family text1'-applied-tokens) "token-font-family"))
|
|
|
|
(t/is (= (count circle1'-applied-tokens) 8))
|
|
(t/is (= (:layout-item-max-h circle1'-applied-tokens) "token-sizing"))
|
|
(t/is (= (:layout-item-min-h circle1'-applied-tokens) "token-sizing"))
|
|
(t/is (= (:layout-item-max-w circle1'-applied-tokens) "token-sizing"))
|
|
(t/is (= (:layout-item-min-w circle1'-applied-tokens) "token-sizing"))
|
|
(t/is (= (:m1 circle1'-applied-tokens) "token-spacing"))
|
|
(t/is (= (:m2 circle1'-applied-tokens) "token-spacing"))
|
|
(t/is (= (:m3 circle1'-applied-tokens) "token-spacing"))
|
|
(t/is (= (:m4 circle1'-applied-tokens) "token-spacing"))))
|
|
|
|
(t/deftest unapply-tokens-from-shape
|
|
(let [;; ==== Setup
|
|
file (-> (setup-file)
|
|
(apply-all-tokens))
|
|
page (thf/current-page file)
|
|
frame1 (ths/get-shape file :frame1)
|
|
text1 (ths/get-shape file :text1)
|
|
circle1 (ths/get-shape file :circle1)
|
|
|
|
;; ==== Action
|
|
changes (-> (-> (pcb/empty-changes nil)
|
|
(pcb/with-page page)
|
|
(pcb/with-objects (:objects page)))
|
|
(cls/generate-update-shapes [(:id frame1)]
|
|
(fn [shape]
|
|
(-> shape
|
|
(cto/unapply-tokens-from-shape [:r1 :r2 :r3 :r4])
|
|
(cto/unapply-tokens-from-shape [:rotation])
|
|
(cto/unapply-tokens-from-shape [:opacity])
|
|
(cto/unapply-tokens-from-shape [:stroke-width])
|
|
(cto/unapply-tokens-from-shape [:stroke-color])
|
|
(cto/unapply-tokens-from-shape [:fill])
|
|
(cto/unapply-tokens-from-shape [:width :height])))
|
|
(:objects page)
|
|
{})
|
|
(cls/generate-update-shapes [(:id text1)]
|
|
(fn [shape]
|
|
(-> shape
|
|
(cto/unapply-tokens-from-shape [:font-size])
|
|
(cto/unapply-tokens-from-shape [:letter-spacing])
|
|
(cto/unapply-tokens-from-shape [:font-family])))
|
|
(:objects page)
|
|
{})
|
|
(cls/generate-update-shapes [(:id circle1)]
|
|
(fn [shape]
|
|
(-> shape
|
|
(cto/unapply-tokens-from-shape [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
|
|
(cto/unapply-tokens-from-shape [:m1 :m2 :m3 :m4])))
|
|
(:objects page)
|
|
{}))
|
|
|
|
file' (thf/apply-changes file changes)
|
|
|
|
;; ==== Get
|
|
frame1' (ths/get-shape file' :frame1)
|
|
frame1'-applied-tokens (:applied-tokens frame1')
|
|
text1' (ths/get-shape file' :text1)
|
|
text1'-applied-tokens (:applied-tokens text1')
|
|
circle1' (ths/get-shape file' :circle1)
|
|
circle1'-applied-tokens (:applied-tokens circle1')]
|
|
|
|
;; ==== Check
|
|
(t/is (= (count frame1'-applied-tokens) 0))
|
|
(t/is (= (count text1'-applied-tokens) 0))
|
|
(t/is (= (count circle1'-applied-tokens) 0))))
|
|
|
|
(t/deftest unapply-tokens-automatic
|
|
(let [;; ==== Setup
|
|
file (-> (setup-file)
|
|
(apply-all-tokens))
|
|
page (thf/current-page file)
|
|
frame1 (ths/get-shape file :frame1)
|
|
text1 (ths/get-shape file :text1)
|
|
circle1 (ths/get-shape file :circle1)
|
|
|
|
;; ==== Action
|
|
changes (-> (-> (pcb/empty-changes nil)
|
|
(pcb/with-page page)
|
|
(pcb/with-objects (:objects page)))
|
|
(cls/generate-update-shapes [(:id frame1)]
|
|
(fn [shape]
|
|
(-> shape
|
|
(ctn/set-shape-attr :r1 0)
|
|
(ctn/set-shape-attr :r2 0)
|
|
(ctn/set-shape-attr :r3 0)
|
|
(ctn/set-shape-attr :r4 0)
|
|
(ctn/set-shape-attr :rotation 0)
|
|
(ctn/set-shape-attr :opacity 0)
|
|
(ctn/set-shape-attr :strokes [])
|
|
(ctn/set-shape-attr :fills [])
|
|
(ctn/set-shape-attr :width 0)
|
|
(ctn/set-shape-attr :height 0)))
|
|
(:objects page)
|
|
{})
|
|
(cls/generate-update-shapes [(:id text1)]
|
|
(fn [shape]
|
|
(txt/update-text-content
|
|
shape
|
|
txt/is-content-node?
|
|
d/txt-merge
|
|
{:fills (ths/sample-fills-color :fill-color "#fabada")
|
|
:font-size "1"
|
|
:letter-spacing "3"
|
|
:font-family "Arial"}))
|
|
(:objects page)
|
|
{})
|
|
(cls/generate-update-shapes [(:id circle1)]
|
|
(fn [shape]
|
|
(-> shape
|
|
(ctn/set-shape-attr :layout-item-max-h 0)
|
|
(ctn/set-shape-attr :layout-item-min-h 0)
|
|
(ctn/set-shape-attr :layout-item-max-w 0)
|
|
(ctn/set-shape-attr :layout-item-min-w 0)
|
|
(ctn/set-shape-attr :layout-item-margin {})))
|
|
(:objects page)
|
|
{}))
|
|
|
|
file' (thf/apply-changes file changes)
|
|
|
|
;; ==== Get
|
|
frame1' (ths/get-shape file' :frame1)
|
|
text1' (ths/get-shape file' :text1)
|
|
circle1' (ths/get-shape file' :circle1)
|
|
applied-tokens-frame' (:applied-tokens frame1')
|
|
applied-tokens-text' (:applied-tokens text1')
|
|
applied-tokens-circle' (:applied-tokens circle1')]
|
|
|
|
;; ==== Check
|
|
(t/is (= (count applied-tokens-frame') 0))
|
|
(t/is (= (count applied-tokens-text') 0))
|
|
(t/is (= (count applied-tokens-circle') 0))))
|
|
|
|
(t/deftest dont-unapply-automatic-when-null-change
|
|
(let [;; ==== Setup
|
|
file (-> (setup-file)
|
|
(tht/apply-token-to-shape :text1 "token-font-size" [:font-size] [:font-size] "14")
|
|
(tht/apply-token-to-shape :text1 "token-letter-spacing" [:letter-spacing] [:letter-spacing] "2")
|
|
(tht/apply-token-to-shape :text1 "token-font-family" [:font-family] [:font-family] ["Helvetica" "Arial" "sans-serif"]))
|
|
page (thf/current-page file)
|
|
text1 (ths/get-shape file :text1)
|
|
|
|
;; ==== Action
|
|
changes (-> (-> (pcb/empty-changes nil)
|
|
(pcb/with-page page)
|
|
(pcb/with-objects (:objects page)))
|
|
;; Some changes in text content are not semantic changes
|
|
;; and thus they don't unapply tokens (e.g. adding an attribute
|
|
;; with value nil or with the default value).
|
|
(cls/generate-update-shapes [(:id text1)]
|
|
(fn [shape]
|
|
(txt/update-text-content
|
|
shape
|
|
txt/is-content-node?
|
|
d/txt-merge
|
|
{:font-size nil
|
|
:line-height "3"
|
|
:nonexistent-attr "sample value"}))
|
|
(:objects page)
|
|
{}))
|
|
|
|
file' (thf/apply-changes file changes)
|
|
|
|
;; ==== Get
|
|
text1' (ths/get-shape file' :text1)
|
|
applied-tokens-text' (:applied-tokens text1')]
|
|
|
|
;; ==== Check
|
|
(t/is (= (count applied-tokens-text') 3)))) |