mirror of
https://github.com/penpot/penpot.git
synced 2026-07-03 04:45:28 +00:00
* ✨ Adds static dispatch safe stubs in tests * 🐛 Fix shapesColors metadata key to match ColorShapeInfo * 🐛 Fix CommentThread.remove rejecting the owner's own threads * 🐛 Fix page.removeCommentThread throwing on a spurious Promise * ✨ Implement ShapeBase.swapComponent in the plugin API * ✨ Expose File.revn in the plugin API * 🐛 Fix FileVersion.createdAt calling Luxon method on a js/Date * 🐛 Fix plugin font/typography application to text and ranges * 🐛 Default plugin overlay interaction position for non-manual types * 🐛 Fix plugin interaction setters passing an id-only shape * 🐛 Fix grid addColumnAtIndex rejecting valid track types * 🐛 Expose libraryId on library color/typography/component proxies * ✨ Implement LibraryTypography.setFont in the plugin API * 🐛 Fix typography.applyToTextRange reading unexposed range bounds * 🐛 Fix utils.geometry.center argument mismatch * 🐛 Fix localStorage.removeItem calling getItem * 🐛 Fix shape backgroundBlur proxy key casing * 🐛 Report boolean shape type as 'boolean' in the plugin API * 🐛 Return the resulting paths from plugin flatten * 🐛 Make plugin z-order methods act on the target shape * 🐛 Make is-variant-container? return a boolean * ✨ Implement Group.isMask in the plugin API * 🐛 Return a shape proxy from TextRange.shape * 🐛 Return the duplicated set from TokenSet.duplicate * 🐛 Fix theme addSet/removeSet reading set name with a keyword * 🐛 Accept string fontFamilies token value in the plugin API * 🐛 Fix combineAsVariants ignoring the passed component ids * 🐛 Fix board removeRulerGuide ignoring its argument * 🐛 Fix board guides setter schema and parser * 🐛 Avoid 0-byte allocation when syncing empty grid tracks * 🐛 Validate grid track indices in the plugin API * 🐛 Return null for empty input in group() and centerShapes() * 🐛 Return TokenTypographyValue[] from a typography token's resolvedValue * 🐛 Return TokenShadowValue[] from a shadow token's resolvedValue * 🐛 Return string[] from a fontFamilies token's resolvedValue * 🐛 Clear mutually-exclusive reps when setting LibraryColor gradient/image * 🐛 Add readonly tags to types, deprecate Image type * 📚 Update plugins changelog
203 lines
9.5 KiB
Clojure
203 lines
9.5 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.plugins.shape-bugfixes-test
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.test-helpers.files :as cthf]
|
|
[app.common.types.component :as ctk]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.workspace :as dw]
|
|
[app.main.data.workspace.variants :as dwv]
|
|
[app.main.store :as st]
|
|
[app.plugins.api :as api]
|
|
[app.plugins.public-utils :as public-utils]
|
|
[app.plugins.shape :as shape]
|
|
[app.plugins.utils :as u]
|
|
[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]))
|
|
|
|
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Helpers
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(defn- child-shapes
|
|
"Ordered child shape ids of `board`, read back from the live store
|
|
(the observable result of a z-order operation)."
|
|
[store ^js context ^js board]
|
|
(let [file-id (aget (. context -currentFile) "$id")
|
|
page-id (aget (. context -currentPage) "$id")
|
|
board-id (aget board "$id")]
|
|
(get-in @store [:files file-id :data :pages-index page-id
|
|
:objects board-id :shapes])))
|
|
|
|
(defn- page-guides
|
|
"The guides map of the current page, read back from the live store."
|
|
[store ^js context]
|
|
(let [file-id (aget (. context -currentFile) "$id")
|
|
page-id (aget (. context -currentPage) "$id")]
|
|
(get-in @store [:files file-id :data :pages-index page-id :guides])))
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
;; Tests
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
(t/deftest trigger-setter-updates-the-interaction-event-type
|
|
;; Regression: the `trigger` setter must update the interaction of the
|
|
;; located shape. Asserting on the observable interaction (read back through
|
|
;; the proxy from the live store) covers that without coupling to which
|
|
;; internal action gets emitted.
|
|
(thw/with-wasm-mocks*
|
|
(fn []
|
|
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
|
^js context (api/create-context plugin-id)
|
|
_ (set! st/state store)
|
|
^js board (.createBoard context)]
|
|
(.addInteraction board "click" #js {:type "open-url" :url "https://example.com"})
|
|
(let [^js interaction (aget (.-interactions board) 0)]
|
|
(t/is (= "click" (.-trigger interaction))
|
|
"the interaction starts with the click trigger")
|
|
(set! (.-trigger interaction) "mouse-over")
|
|
(t/is (= "mouse-over" (.-trigger interaction))
|
|
"the trigger setter updates the interaction event-type"))))))
|
|
|
|
(t/deftest center-shapes-empty-input-returns-nil
|
|
(t/is (nil? (public-utils/centerShapes #js []))))
|
|
|
|
(t/deftest background-blur-reads-background-blur-key
|
|
(let [file-id (uuid/next)
|
|
page-id (uuid/next)
|
|
shape-id (uuid/next)
|
|
blur-id (uuid/next)
|
|
proxy (shape/shape-proxy plugin-id file-id page-id shape-id)]
|
|
(with-redefs [u/proxy->shape (constantly {:background-blur {:id blur-id
|
|
:value 12
|
|
:hidden false}})]
|
|
(let [blur (.-backgroundBlur proxy)]
|
|
(t/is (= (str blur-id) (aget blur "id")))
|
|
(t/is (= 12 (aget blur "value")))))))
|
|
|
|
(t/deftest flatten-returns-proxies-for-converted-shapes
|
|
;; `convert-selected-to-path` runs the WASM boolean/path pipeline, so this
|
|
;; test stays at the proxy boundary: it verifies `flatten` forwards the
|
|
;; selected ids to the conversion and wraps the result back into proxies.
|
|
(let [file-id (uuid/next)
|
|
page-id (uuid/next)
|
|
shape-id (uuid/next)
|
|
input (shape/shape-proxy plugin-id file-id page-id shape-id)
|
|
emitted (atom nil)
|
|
context (api/create-context plugin-id)]
|
|
(set! st/state (atom {:current-file-id file-id
|
|
:current-page-id page-id}))
|
|
(with-redefs [dw/convert-selected-to-path
|
|
(mock/stub (fn [ids]
|
|
(reset! emitted ids)
|
|
:convert-selected-to-path))
|
|
st/emit! mock/noop
|
|
shape/shape-proxy
|
|
(mock/stub (fn [_plugin file page id]
|
|
#js {"$file" file "$page" page "$id" id}))]
|
|
(let [result (.flatten context #js [input])]
|
|
(t/is (= #{shape-id} @emitted))
|
|
(t/is (array? result))
|
|
(t/is (= shape-id (aget result 0 "$id")))
|
|
(t/is (= file-id (aget result 0 "$file")))
|
|
(t/is (= page-id (aget result 0 "$page")))))))
|
|
|
|
(t/deftest z-order-methods-reorder-the-shape-within-its-parent
|
|
;; Asserts the observable child order in the parent after each z-order
|
|
;; method, instead of merely checking which location keyword was emitted.
|
|
;; The assertions are independent of the parent's `:shapes` ordering
|
|
;; convention: a reorder is verified by relative movement and extremes.
|
|
(thw/with-wasm-mocks*
|
|
(fn []
|
|
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
|
^js context (api/create-context plugin-id)
|
|
_ (set! st/state store)
|
|
^js board (.createBoard context)
|
|
children (mapv (fn [_] (.createRectangle context)) (range 4))
|
|
ids (mapv #(aget % "$id") children)
|
|
order #(child-shapes store context board)]
|
|
(doseq [^js c children] (.appendChild board c))
|
|
|
|
;; Operate on a shape that is currently interior (so both a forward
|
|
;; and a backward step are observable).
|
|
(let [mid-id (nth (order) 1)
|
|
^js mid (nth children (d/index-of ids mid-id))]
|
|
|
|
(t/testing "bringForward and sendBackward move in opposite directions"
|
|
(let [i0 (d/index-of (order) mid-id)
|
|
_ (.bringForward mid)
|
|
i1 (d/index-of (order) mid-id)
|
|
_ (.sendBackward mid)
|
|
i2 (d/index-of (order) mid-id)]
|
|
(t/is (not= i0 i1) "bringForward changes the order")
|
|
(t/is (not= i1 i2) "sendBackward changes the order")
|
|
(t/is (= (pos? (- i1 i0)) (neg? (- i2 i1)))
|
|
"the two steps move the shape in opposite directions")))
|
|
|
|
(t/testing "bringToFront and sendToBack move to opposite extremes"
|
|
(let [n (count (order))
|
|
_ (.bringToFront mid)
|
|
p1 (d/index-of (order) mid-id)
|
|
_ (.sendToBack mid)
|
|
p2 (d/index-of (order) mid-id)]
|
|
(t/is (contains? #{0 (dec n)} p1) "bringToFront moves to an extreme")
|
|
(t/is (contains? #{0 (dec n)} p2) "sendToBack moves to an extreme")
|
|
(t/is (not= p1 p2) "front and back are opposite extremes"))))))))
|
|
|
|
(t/deftest is-variant-container-predicate-returns-boolean
|
|
(t/is (false? (ctk/is-variant-container? {})))
|
|
(t/is (true? (ctk/is-variant-container? {:is-variant-container true}))))
|
|
|
|
(t/deftest combine-as-variants-uses-the-passed-component-ids
|
|
;; `combine-as-variants` needs real main components and the variant pipeline,
|
|
;; so this stays at the proxy boundary and verifies the component ids that
|
|
;; the head proxy collects from its argument before delegating.
|
|
(let [file-id (uuid/next)
|
|
page-id (uuid/next)
|
|
head-id (uuid/next)
|
|
other-id (uuid/next)
|
|
proxy (shape/shape-proxy plugin-id file-id page-id head-id)
|
|
captured (atom nil)]
|
|
(with-redefs [u/locate-shape (fn [_file _page id] {:id id :component-id id})
|
|
u/locate-library-component (constantly {:id (uuid/next)})
|
|
ctk/is-variant? (constantly false)
|
|
dwv/combine-as-variants
|
|
(fn [ids opts]
|
|
(reset! captured {:ids ids :opts opts})
|
|
;; return value flows through `se/add-event` (which
|
|
;; calls `with-meta`), so it must support metadata
|
|
{:event :combine-as-variants})
|
|
st/emit! mock/noop
|
|
shape/shape-proxy (mock/stub (fn [& _] #js {}))]
|
|
(.combineAsVariants proxy #js [(str other-id)])
|
|
(t/is (= #{head-id other-id} (:ids @captured))))))
|
|
|
|
(t/deftest remove-ruler-guide-deletes-the-guide-from-the-page
|
|
;; Adds a real ruler guide through the API and asserts it is gone from the
|
|
;; page guides after removeRulerGuide, rather than checking the removal call.
|
|
(thw/with-wasm-mocks*
|
|
(fn []
|
|
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
|
|
^js context (api/create-context plugin-id)
|
|
_ (set! st/state store)
|
|
^js board (.createBoard context)
|
|
^js guide (.addRulerGuide board "horizontal" 10)]
|
|
(t/is (= 1 (count (page-guides store context)))
|
|
"addRulerGuide stores one guide on the page")
|
|
(.removeRulerGuide board guide)
|
|
(t/is (empty? (page-guides store context))
|
|
"removeRulerGuide deletes the guide from the page")))))
|
|
|
|
(t/deftest group-empty-input-returns-nil
|
|
(let [context (api/create-context plugin-id)]
|
|
(t/is (nil? (.group context #js [])))))
|