mirror of
https://github.com/penpot/penpot.git
synced 2026-09-21 11:26:15 +00:00
* 🐛 Fix stroke width token dropdown closing mid-interaction The stroke-row* key included a hash of applied-tokens, forcing a full remount whenever the async token-resolution pipeline updated that state (e.g. after a detach action settles). If the remount landed while the width dropdown was open, it destroyed the dropdown and reset its open state, permanently closing it before the user could pick a token. Drop the hash from the key so the row only remounts on actual structural changes (add/remove/reorder), not on every token resolution tick. AI-assisted-by: claude-sonnet-5 * 🐛 Fix crash when a typography token has a nil value A composite typography token saved with no fields filled in stores a nil :value. That value reached StyleDictionary's tokens-studio font-styles preprocessor, which assumes a typography value is never null and threw, crashing token resolution for every other token in the file. Reject the nil value at the source in the typography token form validation, and as defense in depth, filter nil-valued tokens out of every resolve-tokens* entry point before they reach StyleDictionary, tagging them with the existing empty-input error instead. Also remove a stray debug prn left in find-refs. AI-assisted-by: claude-sonnet-5 * 🔧 Gitignore local CLAUDE.md Keep the commit-conventions instructions file local to each contributor's checkout instead of tracking it in the repo. AI-assisted-by: claude-sonnet-5 * ⚡ Use a single transducer to tag invalid-value tokens merge-invalid-value-tokens ran three separate passes over the token map (remove, map, into) and then merged the result back in. Combine the remove/map steps into one ns-level transducer, defined once instead of rebuilt on every call, and pass resolved as the seed to into so the trailing merge isn't needed either. AI-assisted-by: claude-sonnet-5 * ♻️ Drop redundant t/testing wrapper in nil-value token test The outer t/testing just repeated the deftest's own name and added nothing the two inner t/testing blocks (each covering one concrete assertion group) don't already say. AI-assisted-by: claude-sonnet-5 * 🐛 Fail the nil-value token test on a resolution error rx/sub! only handles the success case, so if token resolution ever errors instead, done is never called and the async test hangs instead of failing. Switch to rx/subs! with an error handler that reports the failure and calls done, matching the pattern already used elsewhere in the tokens test suite. AI-assisted-by: claude-sonnet-5
187 lines
11 KiB
Clojure
187 lines
11 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 INC Sucursal en España SL
|
|
|
|
(ns frontend-tests.tokens.style-dictionary-test
|
|
(:require
|
|
[app.common.test-helpers.ids-map :as cthi]
|
|
[app.common.types.tokens-lib :as ctob]
|
|
[app.main.data.style-dictionary :as sd]
|
|
[beicon.v2.core :as rx]
|
|
[cljs.test :as t :include-macros true]))
|
|
|
|
(t/deftest resolve-tokens-test
|
|
(t/async
|
|
done
|
|
(t/testing "resolves tokens using style-dictionary from a ids map"
|
|
(let [tokens (-> (ctob/make-tokens-lib)
|
|
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :core-set)
|
|
:name "core"))
|
|
(ctob/add-token (cthi/id :core-set)
|
|
(ctob/make-token {:name "borderRadius.sm"
|
|
:value "12px"
|
|
:type :border-radius}))
|
|
(ctob/add-token (cthi/id :core-set)
|
|
(ctob/make-token {:value "{borderRadius.sm} * 2"
|
|
:name "borderRadius.md-with-dashes"
|
|
:type :border-radius}))
|
|
(ctob/add-token (cthi/id :core-set)
|
|
(ctob/make-token {:name "borderRadius.large"
|
|
:value "123456789012345"
|
|
:type :border-radius}))
|
|
(ctob/add-token (cthi/id :core-set)
|
|
(ctob/make-token {:name "borderRadius.largePx"
|
|
:value "123456789012345px"
|
|
:type :border-radius}))
|
|
(ctob/add-token (cthi/id :core-set)
|
|
(ctob/make-token {:name "borderRadius.largeFn"
|
|
:value "{borderRadius.sm} * 200000000"
|
|
:type :border-radius}))
|
|
(ctob/get-all-tokens-map))]
|
|
(-> (sd/resolve-tokens tokens)
|
|
(rx/sub!
|
|
(fn [resolved-tokens]
|
|
(t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value])))
|
|
(t/is (= "px" (get-in resolved-tokens ["borderRadius.sm" :unit])))
|
|
(t/is (= 24 (get-in resolved-tokens ["borderRadius.md-with-dashes" :resolved-value])))
|
|
(t/is (= "px" (get-in resolved-tokens ["borderRadius.md-with-dashes" :unit])))
|
|
(t/is (nil? (get-in resolved-tokens ["borderRadius.large" :resolved-value])))
|
|
(t/is (= :error.token/number-too-large
|
|
(get-in resolved-tokens ["borderRadius.large" :errors 0 :error/code])))
|
|
(t/is (nil? (get-in resolved-tokens ["borderRadius.largePx" :resolved-value])))
|
|
(t/is (= :error.token/number-too-large
|
|
(get-in resolved-tokens ["borderRadius.largePx" :errors 0 :error/code])))
|
|
(t/is (nil? (get-in resolved-tokens ["borderRadius.largeFn" :resolved-value])))
|
|
(t/is (= :error.token/number-too-large
|
|
(get-in resolved-tokens ["borderRadius.largeFn" :errors 0 :error/code])))
|
|
(done))))))))
|
|
|
|
;; Regression for #9584 — when one active set defines a token named
|
|
;; "a" and another defines "a.b", the tokens-tree builder collapses
|
|
;; them via assoc-in, so StyleDictionary only sees one. Previously
|
|
;; the other vanished from the sidebar entirely; now `resolve-tokens`
|
|
;; tags the dropped token with `:error.token/name-collision` so the
|
|
;; existing broken-pill rendering picks it up.
|
|
|
|
(t/deftest resolve-tokens-name-collision-test
|
|
(t/async
|
|
done
|
|
(t/testing "tokens colliding with a token-group prefix survive resolution as broken pills"
|
|
(let [tokens (-> (ctob/make-tokens-lib)
|
|
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-1)
|
|
:name "set-1"))
|
|
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-2)
|
|
:name "set-2"))
|
|
(ctob/add-token (cthi/id :set-1)
|
|
(ctob/make-token {:name "a"
|
|
:value "8px"
|
|
:type :border-radius}))
|
|
(ctob/add-token (cthi/id :set-2)
|
|
(ctob/make-token {:name "a.b"
|
|
:value "12px"
|
|
:type :border-radius}))
|
|
(ctob/get-all-tokens-map))]
|
|
(-> (sd/resolve-tokens tokens)
|
|
(rx/sub!
|
|
(fn [resolved-tokens]
|
|
(t/testing "both tokens are present in the resolved map"
|
|
(t/is (contains? resolved-tokens "a"))
|
|
(t/is (contains? resolved-tokens "a.b")))
|
|
(t/testing "the colliding token carries the name-collision error"
|
|
(let [errors (or (get-in resolved-tokens ["a" :errors])
|
|
(get-in resolved-tokens ["a.b" :errors]))]
|
|
(t/is (seq errors))
|
|
(t/is (= :error.token/name-collision
|
|
(-> errors first :error/code)))))
|
|
(done))))))))
|
|
|
|
;; Regression: a composite typography token whose value is a plain
|
|
;; array (e.g. ["Roboto"]) instead of a map must not crash with
|
|
;; "No protocol method IMap.-dissoc defined for type object".
|
|
;; It should return an invalid-token-value-typography error instead.
|
|
(t/deftest resolve-tokens-typography-array-value-test
|
|
(t/async
|
|
done
|
|
(t/testing "typography token with array value produces error instead of crashing"
|
|
(let [tokens (-> (ctob/make-tokens-lib)
|
|
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :core-set)
|
|
:name "core"))
|
|
(ctob/add-token (cthi/id :core-set)
|
|
(ctob/make-token {:name "typography.bad"
|
|
:value ["Roboto"]
|
|
:type :typography}))
|
|
(ctob/get-all-tokens-map))]
|
|
(-> (sd/resolve-tokens tokens)
|
|
(rx/sub!
|
|
(fn [resolved-tokens]
|
|
(t/is (contains? resolved-tokens "typography.bad"))
|
|
(t/is (nil? (get-in resolved-tokens ["typography.bad" :resolved-value])))
|
|
(t/is (= :error.style-dictionary/invalid-token-value-typography
|
|
(get-in resolved-tokens ["typography.bad" :errors 0 :error/code])))
|
|
(done))))))))
|
|
|
|
;; Regression: a token with a `nil` value (e.g. a composite typography
|
|
;; token saved via the workspace form with no fields filled in) must never
|
|
;; reach StyleDictionary — its `tokens-studio` preprocessor assumes a
|
|
;; typography token's value is never null and throws an uncaught exception
|
|
;; on it, which used to take down resolution for every other token in the
|
|
;; file. It should be tagged with an empty-input error instead.
|
|
(t/deftest resolve-tokens-nil-value-test
|
|
(t/async
|
|
done
|
|
(let [tokens (-> (ctob/make-tokens-lib)
|
|
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :core-set)
|
|
:name "core"))
|
|
(ctob/add-token (cthi/id :core-set)
|
|
(ctob/make-token {:name "typography.empty"
|
|
:value nil
|
|
:type :typography}))
|
|
(ctob/add-token (cthi/id :core-set)
|
|
(ctob/make-token {:name "borderRadius.sm"
|
|
:value "12px"
|
|
:type :border-radius}))
|
|
(ctob/get-all-tokens-map))]
|
|
(->> (sd/resolve-tokens tokens)
|
|
(rx/subs!
|
|
(fn [resolved-tokens]
|
|
(t/testing "the nil-value token is tagged with an error instead of crashing"
|
|
(t/is (contains? resolved-tokens "typography.empty"))
|
|
(t/is (nil? (get-in resolved-tokens ["typography.empty" :resolved-value])))
|
|
(t/is (= :error.token/empty-input
|
|
(get-in resolved-tokens ["typography.empty" :errors 0 :error/code]))))
|
|
(t/testing "other tokens still resolve normally"
|
|
(t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value])))))
|
|
(fn [err]
|
|
(t/do-report {:type :error :message "Stream error" :actual err})
|
|
(done))
|
|
(fn []
|
|
(done)))))))
|
|
|
|
(t/deftest resolve-tokens-interactive-test
|
|
(t/async
|
|
done
|
|
(t/testing "resolves tokens interactively using backtrace ids map"
|
|
(let [tokens (-> (ctob/make-tokens-lib)
|
|
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :core-set)
|
|
:name "core"))
|
|
(ctob/add-token (cthi/id :core-set)
|
|
(ctob/make-token {:name "borderRadius.sm"
|
|
:value "12px"
|
|
:type :border-radius}))
|
|
(ctob/add-token (cthi/id :core-set)
|
|
(ctob/make-token {:value "{borderRadius.sm} * 2"
|
|
:name "borderRadius.md"
|
|
:type :border-radius}))
|
|
(ctob/get-all-tokens-map))]
|
|
(-> (sd/resolve-tokens-interactive tokens)
|
|
(rx/sub!
|
|
(fn [resolved-tokens]
|
|
(t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value])))
|
|
(t/is (= "px" (get-in resolved-tokens ["borderRadius.sm" :unit])))
|
|
(t/is (= 24 (get-in resolved-tokens ["borderRadius.md" :resolved-value])))
|
|
(t/is (= "px" (get-in resolved-tokens ["borderRadius.md" :unit])))
|
|
(done))))))))
|
|
|