penpot/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs
Renzo 02ab41f420
🐛 Token remap preserves child component sync after renaming a token group (#9566)
* 🐛 Token remap preserves child component sync after renaming a token group

* 📚 Do not update CHANGES.md

We are changing the procedures to not update the changelog on each PR. Instead, we use github tracking to check what issues come in a release, and update the changelog automatically in a batch.

Signed-off-by: Andrés Moya <andres.moya@kaleidos.net>

---------

Signed-off-by: Andrés Moya <andres.moya@kaleidos.net>
Co-authored-by: Andrés Moya <andres.moya@kaleidos.net>
2026-05-26 15:46:53 +02:00

228 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
(ns frontend-tests.tokens.logic.token-remapping-test
(: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.shapes :as cths]
[app.common.test-helpers.tokens :as ctht]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.remapping :as dwtr]
[cljs.test :as t :include-macros true]))
(defn setup-file-with-tokens
"Setup a test file with tokens and shapes that use those tokens"
[]
(let [color-token {:id (cthi/new-id! :color-primary)
:name "color.primary"
:value "#FF0000"
:type :color}
alias-token {:id (cthi/new-id! :color-secondary)
:name "color.secondary"
:value "{color.primary}"
:type :color}]
(-> (cthf/sample-file :file-1 :page-label :page-1)
(ctho/add-rect :rect-1)
(ctho/add-rect :rect-2)
(assoc-in [:data :tokens-lib]
(-> (ctob/make-tokens-lib)
(ctob/add-theme (ctob/make-token-theme :name "Theme A" :sets #{"Set A"}))
(ctob/set-active-themes #{"/Theme A"})
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a)
:name "Set A"))
(ctob/add-token (cthi/id :set-a)
(ctob/make-token color-token))
(ctob/add-token (cthi/id :set-a)
(ctob/make-token alias-token))))
;; Apply the token to rect-1
(ctht/apply-token-to-shape :rect-1 "color.primary" [:fill] [:fill] "#FF0000"))))
(t/deftest test-scan-token-value-references
(t/testing "should extract token references from alias values"
(let [token {:id (cthi/id :color-primary)
:name "color.secondary"
:value "{color.primary}"
:type :color}
references (dwtr/scan-token-value-references token "color.primary")]
(t/is (= 1 (count references)))
(let [ref (first references)]
(t/is (= :token-alias (:type ref)))
(t/is (= (cthi/id :color-secondary) (:source-token-id ref)))
(t/is (= "color.primary" (:referenced-token-name ref)))))))
(t/deftest test-scan-workspace-token-references
(t/testing "should find applied token references"
(let [file (setup-file-with-tokens)
file-data (:data file)
scan-results (dwtr/scan-workspace-token-references file-data "color.primary")]
(t/is (= 1 (count (:applied-tokens scan-results))))
(t/is (= 1 (count (:token-aliases scan-results))))
(t/is (= 2 (:total-references scan-results)))
;; Check applied token reference
(let [applied-ref (first (:applied-tokens scan-results))]
(t/is (= :applied-token (:type applied-ref)))
(t/is (= "color.primary" (:token-name applied-ref)))
(t/is (= :fill (:attribute applied-ref))))
;; Check alias reference
(let [alias-ref (first (:token-aliases scan-results))]
(t/is (= :token-alias (:type alias-ref)))
(t/is (= (cthi/id :color-secondary) (:source-token-id alias-ref)))
(t/is (= "color.primary" (:referenced-token-name alias-ref)))))))
(t/deftest test-update-token-value-references
(t/testing "should update token references in alias values"
(let [old-value "{color.primary}"
new-value (cto/update-token-value-references old-value "color.primary" "brand.primary")]
(t/is (= "{brand.primary}" new-value))))
(t/testing "should update multiple references"
(let [old-value "calc({spacing.base} + {spacing.base})"
new-value (cto/update-token-value-references old-value "spacing.base" "spacing.foundation")]
(t/is (= "calc({spacing.foundation} + {spacing.foundation})" new-value))))
(t/testing "should not update partial matches"
(let [old-value "{color.primary.light}"
new-value (cto/update-token-value-references old-value "color.primary" "brand.primary")]
(t/is (= "{color.primary.light}" new-value)))))
(t/deftest test-count-token-references
(t/testing "should count total references to a token"
(let [file (setup-file-with-tokens)
file-data (:data file)
count (dwtr/count-token-references file-data "color.primary")]
(t/is (= 2 count)))))
(t/deftest test-validate-token-remapping
(t/testing "should validate remapping parameters"
(t/testing "empty name should be invalid"
(let [result (dwtr/validate-token-remapping "color.primary" "")]
(t/is (false? (:valid? result)))
(t/is (= :invalid-name (:error result)))))
(t/testing "same name should be invalid"
(let [result (dwtr/validate-token-remapping "color.primary" "color.primary")]
(t/is (false? (:valid? result)))
(t/is (= :no-change (:error result)))))
(t/testing "valid new name should be valid"
(let [result (dwtr/validate-token-remapping "color.primary" "brand.primary")]
(t/is (true? (:valid? result)))))))
(defn- setup-file-with-component-copy-and-token
"Create a file containing a component, an instance copy of it, and a single
`color.primary` token already applied to both the main and the copy shape.
Mirrors the in-app state right after a user applies a token to a main and
component sync has propagated the applied-tokens map to the copy."
[]
(let [color-primary {:id (cthi/new-id! :color-primary)
:name "color.primary"
:value "#FF0000"
:type :color}
color-secondary {:id (cthi/new-id! :color-secondary)
:name "color.secondary"
:value "#00FF00"
:type :color}]
(-> (cthf/sample-file :file-1 :page-label :page-1)
(ctho/add-simple-component-with-copy :component1
:main-root
:main-child
:copy-root
:copy-root-params {:children-labels [:copy-child]})
(assoc-in [:data :tokens-lib]
(-> (ctob/make-tokens-lib)
(ctob/add-theme (ctob/make-token-theme :name "Theme A" :sets #{"Set A"}))
(ctob/set-active-themes #{"/Theme A"})
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a)
:name "Set A"))
(ctob/add-token (cthi/id :set-a)
(ctob/make-token color-primary))
(ctob/add-token (cthi/id :set-a)
(ctob/make-token color-secondary))))
(ctht/apply-token-to-shape :main-child "color.primary" [:fill] [:fill] "#FF0000")
(ctht/apply-token-to-shape :copy-child "color.primary" [:fill] [:fill] "#FF0000"))))
(t/deftest test-remap-tokens-finds-both-main-and-copy
(t/testing "scan should find applied-token references on both main and copy shapes"
(let [file (setup-file-with-component-copy-and-token)
scan-results (dwtr/scan-workspace-token-references (:data file) "color.primary")
shape-ids (set (map :shape-id (:applied-tokens scan-results)))]
(t/is (= 2 (count (:applied-tokens scan-results))))
(t/is (contains? shape-ids (cthi/id :main-child)))
(t/is (contains? shape-ids (cthi/id :copy-child))))))
(t/deftest test-remap-tokens-does-not-touch-copy
(t/testing "renaming a token must not flip the copy's sync group to :touched"
(let [;; The production flow first renames the token in the tokens-lib,
;; then dispatches remap-tokens to update applied-token references
;; on shapes. Mirror that order here.
file (-> (setup-file-with-component-copy-and-token)
(update-in [:data :tokens-lib]
(fn [lib]
(ctob/update-token lib
(cthi/id :set-a)
(cthi/id :color-primary)
#(assoc % :name "colors.primary")))))
changes (dwtr/build-remap-changes (:data file)
"color.primary"
"colors.primary")
file' (cthf/apply-changes file changes)
main-child' (cths/get-shape file' :main-child)
copy-child' (cths/get-shape file' :copy-child)]
(t/is (= "colors.primary" (get-in main-child' [:applied-tokens :fill])))
(t/is (= "colors.primary" (get-in copy-child' [:applied-tokens :fill])))
;; If the rename marks :fill-group as touched on the copy, future
;; main→copy propagation will skip it — that is the #9495 regression.
(t/is (not (contains? (set (:touched copy-child')) :fill-group))))))
(t/deftest test-remap-preserves-copy-sync-of-later-token-apply
(t/testing "End-to-end #9495 scenario: after a token group rename + REMAP,
applying a new token to the main must still propagate to the copy."
(let [;; 1. Build file with two tokens, a component, and color.primary
;; already applied to both main-child and copy-child.
file (setup-file-with-component-copy-and-token)
;; 2. Rename the token group: color.primary -> colors.primary,
;; color.secondary -> colors.secondary (mimics user editing the
;; group name in the tokens panel).
file (update-in file [:data :tokens-lib]
(fn [lib]
(-> lib
(ctob/update-token (cthi/id :set-a) (cthi/id :color-primary)
#(assoc % :name "colors.primary"))
(ctob/update-token (cthi/id :set-a) (cthi/id :color-secondary)
#(assoc % :name "colors.secondary")))))
;; 3. REMAP runs to update applied-token names on shapes.
file (cthf/apply-changes
file
(dwtr/build-remap-changes (:data file)
"color.primary"
"colors.primary"))
;; 4. User then applies colors.secondary to the main (Step 7).
file (ctht/apply-token-to-shape file :main-child "colors.secondary"
[:fill] [:fill] "#00FF00")
;; 5. Component sync propagates the main's change to the copy.
file (ctho/propagate-component-changes file :component1)
copy-child' (cths/get-shape file :copy-child)]
;; Step 8 of the issue: the copy must reflect the newly applied token.
;; Pre-fix, REMAP marks :fill-group as touched on the copy, so the
;; subsequent sync silently skips :applied-tokens and the copy is left
;; pointing at colors.primary.
(t/is (= "colors.secondary" (get-in copy-child' [:applied-tokens :fill]))))))