From 30849babccd733600d10ed1ee58d1d3185b5c0b9 Mon Sep 17 00:00:00 2001 From: makesomethingshit Date: Thu, 10 Sep 2026 20:43:41 +0900 Subject: [PATCH] :bug: Fix fontFamilies token property mapping in Plugin API (#11566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: Fix fontFamilies token property mapping in Plugin API The Plugin API exposes the font-family token property as `fontFamilies`, while Penpot stores the canonical applied-token attribute as `:font-family`. The bidirectional plugin/internal attribute map did not contain that alias, so explicit `applyToken(..., ["fontFamilies"])` validation rejected the property and applied-token readback exposed the undocumented singular `fontFamily`. Add `:font-family -> :font-families` to the existing canonical alias map. The reverse mapping is derived automatically, keeping application and readback symmetric without introducing a font-specific code path. Closes #11405 AI-assisted-by: Omen Alpha Signed-off-by: 최준수 * :bug: Fix fontFamilies e2e test to target a text shape The fontFamilies end-to-end regression created a flex layout frame, whose attribute set (frame-with-layout-attributes) excludes :font-family. The workspace token application filters such shapes, so the internal binding and readback assertions would pass vacuously without exercising the alias. Target an actual `:text` shape (ctho/add-text) instead, so the test verifies the full JS "fontFamilies" -> schema -> alias -> canonical :font-family -> camelCase readback path. AI-assisted-by: Omen Alpha Signed-off-by: 최준수 * :bug: Fix fontFamilies test WASM error and add changelog entry The text-shape fontFamilies e2e applies a layout-affecting token via wasm renderer path, hitting missing WASM exports under Node. Merge thw/setup-wasm-mocks! into the :each fixture and add plugins CHANGELOG entry for the fontFamilies alias fix. AI-assisted-by: muse-spark-1.3-contributor Related to #11566 Signed-off-by: makesomethingshit --------- Signed-off-by: 최준수 Signed-off-by: makesomethingshit --- frontend/src/app/plugins/tokens.cljs | 4 +- .../frontend_tests/plugins/tokens_test.cljs | 70 ++++++++++++++++++- plugins/CHANGELOG.md | 1 + 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index f47debeaff..b27019c7ba 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -41,7 +41,9 @@ :m1 :margin-top :m2 :margin-right :m3 :margin-bottom - :m4 :margin-left}) + :m4 :margin-left + + :font-family :font-families}) (def ^:private map:token-attr-plugin->token-attr (merge diff --git a/frontend/test/frontend_tests/plugins/tokens_test.cljs b/frontend/test/frontend_tests/plugins/tokens_test.cljs index 620e0cce7c..3f4f56f6d9 100644 --- a/frontend/test/frontend_tests/plugins/tokens_test.cljs +++ b/frontend/test/frontend_tests/plugins/tokens_test.cljs @@ -22,9 +22,14 @@ [cljs.test :as t :include-macros true] [frontend-tests.helpers.mock :as mock] [frontend-tests.helpers.state :as ths] + [frontend-tests.helpers.wasm :as thw] [potok.v2.core :as ptk])) -(t/use-fixtures :each {:before cthi/reset-idmap!}) +(t/use-fixtures :each + {:before (fn [] + (cthi/reset-idmap!) + (thw/setup-wasm-mocks!)) + :after thw/teardown-wasm-mocks!}) (def ^:private get-resolved-value @#'ptok/get-resolved-value) @@ -81,6 +86,18 @@ (t/is (= :m3 (ptok/token-attr-plugin->token-attr :margin-bottom))) (t/is (= :m4 (ptok/token-attr-plugin->token-attr :margin-left)))) +(t/deftest token-attr-plugin->token-attr-resolves-font-family-alias + ;; Plugin-facing `fontFamilies` (kebab-cased to `:font-families` by the + ;; schema layer) maps to the canonical internal `:font-family`. + (t/is (= :font-family (ptok/token-attr-plugin->token-attr :font-families))) + (t/is (= :font-family (ptok/token-attr-plugin->token-attr "font-families")))) + +(t/deftest token-attr->token-attr-plugin-resolves-font-family-alias + ;; Symmetric direction: the canonical internal attribute maps to the + ;; plural plugin-facing name so applied-token readback serializes as + ;; camelCase `fontFamilies`, not the undocumented singular `fontFamily`. + (t/is (= :font-families (ptok/token-attr->token-attr-plugin :font-family)))) + (t/deftest token-attr-plugin->token-attr-coerces-string-input ;; This is the actual regression — JS plugin calls supply strings. (t/is (= :fill (ptok/token-attr-plugin->token-attr "fill"))) @@ -150,6 +167,57 @@ (done))) 0)))) +(t/deftest shape-apply-token-accepts-font-families + (t/async + done + (let [set-id (cthi/new-id! :token-set) + token-id (cthi/new-id! :font-family-token) + file (-> (cthf/sample-file :file1 :page-label :page1) + (ctho/add-text :text1 "Hello World!") + (ctht/add-tokens-lib) + (ctht/update-tokens-lib + #(-> % + (ctob/add-set + (ctob/make-token-set :id set-id + :name "fonts")) + (ctob/add-theme + (ctob/make-token-theme :name "theme" + :sets #{"fonts"})) + (ctob/set-active-themes #{"/theme"}) + (ctob/add-token + set-id + (ctob/make-token :id token-id + :name "font.primary" + :type :font-family + :value ["Inter"]))))) + store (ths/setup-store file) + _ (set! st/state store) + _ (set! st/stream (ptk/input-stream store)) + ^js context (api/create-context "00000000-0000-0000-0000-000000000000") + ^js page (.-currentPage context) + ^js shape (.getShapeById page (str (cthi/id :text1))) + ^js library (.-library context) + ^js local (.-local library) + ^js catalog (.-tokens local) + ^js token-set (.getSetById catalog (str set-id)) + ^js token (.getTokenById token-set (str token-id))] + (.applyToken shape token #js ["fontFamilies"]) + (js/setTimeout + (fn [] + (let [shape-id (cthi/id :text1) + page-id (cthf/current-page-id file)] + ;; Plugin readback exposes the documented plural key. + (t/is (= "font.primary" (.. shape -tokens -fontFamilies))) + ;; The undocumented singular spelling must not leak. + (t/is (undefined? (.. shape -tokens -fontFamily))) + ;; Internal state keeps the canonical `:font-family` key. + (t/is (= "font.primary" + (get-in @store + [:files (:id file) :data :pages-index page-id + :objects shape-id :applied-tokens :font-family]))) + (done))) + 0)))) + (t/deftest token-attr?-rejects-unknown-input (t/is (false? (boolean (ptok/token-attr? :not-a-real-attr)))) (t/is (false? (boolean (ptok/token-attr? "not-a-real-attr")))) diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 63163e2aca..96bd5dc421 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -11,6 +11,7 @@ - **plugins-runtime**: `Library.createComponent()` now rejects invalid input (an empty shape list, or a shape inside a component copy) with a validation error instead of returning a component proxy pointing at nothing. - **plugins-runtime**: Setting an individual padding/margin side (`leftPadding`, `topMargin`, …) now re-derives the padding/margin type, switching to `multiple` when the four sides stop being symmetric (so the value is actually painted) and back to `simple` once top/bottom and left/right are mirrored again. - **plugins-runtime**: Removed the premature deep-hardening of the host plugin context, which froze shared host functions (including `Function.prototype`) before SES override taming, causing `TypeError: Cannot assign to read only property 'toString'` on later host-side function extension. Related to #11001. +- **plugins-runtime**: Fixed the `fontFamilies` token property mapping so `Shape.applyToken(token, ["fontFamilies"])` resolves to the canonical `:font-family` attribute and applied-token readback exposes the documented `fontFamilies` key instead of the undocumented singular `fontFamily`. Closes #11405. ## 1.5.0 (2026-07-08)