diff --git a/frontend/src/app/plugins/format.cljs b/frontend/src/app/plugins/format.cljs index c0b556bdab..36137f1307 100644 --- a/frontend/src/app/plugins/format.cljs +++ b/frontend/src/app/plugins/format.cljs @@ -22,9 +22,10 @@ (when kw (d/name kw))) (defn format-array + "Formats a collection into a JS array, applying `format-fn` to each item. + Always returns an array; an empty array is returned for a nil/empty `coll`." [format-fn coll] - (when (some? coll) - (apply array (keep format-fn coll)))) + (apply array (keep format-fn coll))) (defn format-mixed @@ -174,9 +175,7 @@ (defn format-shadows [shadows] - (if (some? shadows) - (format-array format-shadow shadows) - (array))) + (format-array format-shadow shadows)) ;;export interface Fill { ;; fillColor?: string; @@ -258,8 +257,7 @@ (defn format-exports [exports] - (when (some? exports) - (format-array format-export exports))) + (format-array format-export exports)) ;; export interface GuideColumnParams { ;; color: { color: string; opacity: number }; @@ -341,8 +339,7 @@ (defn format-frame-guides [guides] - (when (some? guides) - (format-array format-frame-guide guides))) + (format-array format-frame-guide guides)) ;;interface PathCommand { ;; command: @@ -396,8 +393,7 @@ (defn format-path-content [content] - (when (some? content) - (format-array format-command content))) + (format-array format-command content)) ;; export type TrackType = 'flex' | 'fixed' | 'percent' | 'auto'; ;; @@ -414,8 +410,7 @@ (defn format-tracks [tracks] - (when (some? tracks) - (format-array format-track tracks))) + (format-array format-track tracks)) ;; export interface Dissolve { diff --git a/frontend/test/frontend_tests/plugins/context_shapes_test.cljs b/frontend/test/frontend_tests/plugins/context_shapes_test.cljs index 4b0af4cbfe..dd1a7e3634 100644 --- a/frontend/test/frontend_tests/plugins/context_shapes_test.cljs +++ b/frontend/test/frontend_tests/plugins/context_shapes_test.cljs @@ -382,3 +382,23 @@ (t/is (pos? (thw/call-count :clean-modifiers))) (t/is (pos? (thw/call-count :set-structure-modifiers))) (t/is (pos? (thw/call-count :propagate-modifiers)))))))) + +(t/deftest test-array-properties-return-empty-array-when-no-items + ;; Array-typed properties must always return an array, never null, + ;; even when the shape has no items for that property. + (thw/with-wasm-mocks* + (fn [] + (let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1)) + ^js context (api/create-context "00000000-0000-0000-0000-000000000000") + _ (set! st/state store) + ^js shape (.createRectangle context)] + + (t/testing " - exports (no exports set)" + (let [exports (.-exports shape)] + (t/is (array? exports)) + (t/is (= 0 (.-length exports))))) + + (t/testing " - shadows (no shadows set)" + (let [shadows (.-shadows shape)] + (t/is (array? shadows)) + (t/is (= 0 (.-length shadows))))))))) diff --git a/frontend/test/frontend_tests/plugins/format_test.cljs b/frontend/test/frontend_tests/plugins/format_test.cljs new file mode 100644 index 0000000000..2bc525b224 --- /dev/null +++ b/frontend/test/frontend_tests/plugins/format_test.cljs @@ -0,0 +1,40 @@ +;; 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.plugins.format-test + (:require + [app.plugins.format :as format] + [cljs.test :as t :include-macros true])) + +(t/deftest test-format-array-always-returns-array + (t/testing "nil collection returns an empty array" + (let [result (format/format-array identity nil)] + (t/is (array? result)) + (t/is (= 0 (.-length result))))) + + (t/testing "empty collection returns an empty array" + (let [result (format/format-array identity [])] + (t/is (array? result)) + (t/is (= 0 (.-length result))))) + + (t/testing "non-empty collection maps each item" + (let [result (format/format-array inc [1 2 3])] + (t/is (array? result)) + (t/is (= [2 3 4] (vec result))))) + + (t/testing "items dropped by format-fn (nil) are removed" + (let [result (format/format-array #(when (odd? %) %) [1 2 3 4])] + (t/is (= [1 3] (vec result)))))) + +(t/deftest test-array-wrappers-return-empty-array-on-nil + ;; Each wrapper backs a non-nullable array-typed Plugin API property and + ;; must return an empty array (never nil) when the source collection is nil. + (t/are [result] (and (array? result) (= 0 (.-length result))) + (format/format-shadows nil) + (format/format-exports nil) + (format/format-frame-guides nil) + (format/format-tracks nil) + (format/format-path-content nil))) diff --git a/frontend/test/frontend_tests/plugins/page_test.cljs b/frontend/test/frontend_tests/plugins/page_test.cljs index 5dd97e6aec..f85cea4329 100644 --- a/frontend/test/frontend_tests/plugins/page_test.cljs +++ b/frontend/test/frontend_tests/plugins/page_test.cljs @@ -100,6 +100,15 @@ (mock-page-initialized store page2-id)))) +(t/deftest test-flows-returns-empty-array-when-no-flows + ;; page.flows must always return an array, even when the page has no flows + (let [{:keys [context]} (setup) + ^js pages (.. context -currentFile -pages) + ^js page1 (aget pages 0) + ^js flows (.-flows page1)] + (t/is (array? flows)) + (t/is (= 0 (.-length flows))))) + (t/deftest test-open-page-does-not-resolve-for-wrong-page ;; Promise should not resolve when a different page is initialized (t/async done diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index e625900a35..b3b16a2079 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -28,6 +28,7 @@ [frontend-tests.logic.pasting-in-containers-test] [frontend-tests.main-errors-test] [frontend-tests.plugins.context-shapes-test] + [frontend-tests.plugins.format-test] [frontend-tests.plugins.interactions-test] [frontend-tests.plugins.page-active-validation-test] [frontend-tests.plugins.page-test] @@ -85,6 +86,7 @@ frontend-tests.plugins.context-shapes-test frontend-tests.plugins.page-active-validation-test frontend-tests.plugins.interactions-test + frontend-tests.plugins.format-test frontend-tests.plugins.page-test frontend-tests.plugins.parser-test frontend-tests.plugins.tokens-test diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 74d0d8bfa3..64163dbd42 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -2,6 +2,7 @@ - **plugins-runtime**: changes outside the current page now raise a validation error when the target belongs to a page that is not currently active, instead of silently operating on the active page. - **plugins-runtime**: Fix inverted validation that rejected valid values (and accepted invalid ones) on text range `align`, `direction`, `textDecoration`, `letterSpacing` and on layout child `zIndex`. +- **plugins-runtime**: Array-typed properties (e.g. `page.flows`, `shape.exports`, `shape.shadows`, layout `rows`/`columns`, ruler guides, path `commands`) now always return an array, returning an empty array instead of `null` when there are no items - **plugins-runtime**: Added `version` field that returns the current version - **plugins-runtime**: Added optional parameter `throwOnError` to `penpot.ui.sendMessage` (default false, backwards-compatible) - **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering`