Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2026-07-28 11:00:47 +02:00
commit 535ccfb930
18 changed files with 502 additions and 75 deletions

View File

@ -29,6 +29,7 @@ function isDefined(v) {
}
function mergeBlockData(block, newData) {
if (!block) return undefined;
let data = block.getData();
for (let key of Object.keys(newData)) {
@ -176,10 +177,12 @@ export function splitBlockPreservingData(state) {
content = Modifier.splitBlock(content, selection);
const blockData = content.blockMap.get(content.selectionBefore.getStartKey()).getData();
const startKey = content.selectionBefore.getStartKey();
const block = content.blockMap.get(startKey);
const blockData = (block && block.getData()) || new Map();
const blockKey = content.selectionAfter.getStartKey();
const blockMap = content.blockMap.update(blockKey, (block) => {
return block.set("data", blockData);
const blockMap = content.blockMap.update(blockKey, (b) => {
return b.set("data", blockData);
});
content = content.set("blockMap", blockMap);
@ -325,6 +328,7 @@ export function updateBlockData(state, blockKey, data) {
const content = state.getCurrentContent();
const block = content.getBlockForKey(blockKey);
const newBlock = mergeBlockData(block, data);
if (!newBlock) return state;
const blockData = newBlock.getData();

View File

@ -275,7 +275,11 @@
;; the event loop
expand-s (->> (rx/of (dwc/expand-all-parents ids objects))
(rx/observe-on :async))
interrupt-s (rx/of :interrupt ::dwsp/interrupt)]
;; :interrupt aborts drag-stopper; only emit it when clearing edition
;; (unconditional emit broke marquee selection after #10798).
interrupt-s (if (some? (dm/get-in state [:workspace-local :edition]))
(rx/of :interrupt ::dwsp/interrupt)
(rx/of ::dwsp/interrupt))]
(rx/merge expand-s interrupt-s)))))
(defn select-all

View File

@ -107,8 +107,8 @@
(defn format-last-events
"Render the `last-events` buffer as a multi-line string with the
wall-clock time of each event and the delta (ms) since the previous
entry. The first entry has no delta. Useful for embedding in error
reports."
entry. The delta column is right-padded to 10 chars so the event
names align. Useful for embedding in error reports."
([] (format-last-events @last-events))
([events]
(let [lines
@ -117,13 +117,14 @@
out (transient [])]
(if xs
(let [{:keys [name t]} (first xs)
iso (ct/format-inst t :iso)
tail (if prev-t
(str " (+" (ct/diff-ms prev-t t) "ms)")
"")]
iso (ct/format-inst t :iso)
delta (if prev-t
(str "(+" (ct/diff-ms prev-t t) "ms)")
"(+0ms)")
delta-pad (str/pad delta {:length 10 :type :right})]
(recur t
(next xs)
(conj! out (str iso tail " " name))))
(conj! out (str iso " " delta-pad " " name))))
(persistent! out)))]
(str/join "\n" lines))))

View File

@ -449,3 +449,190 @@
(on-submit form event))))]
[:> (mf/provider form-ctx) {:value form}
[:form {:class class :on-submit on-submit'} children]]))
(defn- conj-dedup
"A helper that adds item into a vector and removes possible
duplicates. This is not very efficient implementation but is ok for
handling form input that will have a small number of items."
[coll item]
(into [] (distinct) (conj coll item)))
(mf/defc multi-input
[{:keys [form label class trim valid-item-fn caution-item-fn on-submit] :as props}]
(let [form (or form (mf/use-ctx form-ctx))
input-name (get props :name)
touched? (get-in @form [:touched input-name])
error (get-in @form [:errors input-name])
focus? (mf/use-state false)
auto-focus? (get props :auto-focus? false)
items (mf/use-state
(fn []
(let [initial (get-in @form [:data input-name])]
(if (or (vector? initial)
(set? initial))
(mapv (fn [val]
{:text val
:valid (valid-item-fn val)
:caution (caution-item-fn val)})
initial)
[]))))
value (mf/use-state "")
result (hooks/use-equal-memo @items)
empty? (and (str/empty? @value)
(zero? (count @items)))
klass (str (get props :class) " "
(stl/css-case
:focus @focus?
:valid (and touched? (not error))
:invalid (and touched? error)
:empty empty?
:custom-multi-input true))
in-klass (str class " "
(stl/css-case
:inside-input true
:no-padding (pos? (count @items))
:invalid (and (some? valid-item-fn)
touched?
(not (str/empty? @value))
(not (valid-item-fn @value)))))
on-focus
(mf/use-fn #(reset! focus? true))
on-change
(mf/use-fn
(fn [event]
(let [content (-> event dom/get-target dom/get-input-value)]
(reset! value content))))
update-form!
(mf/use-fn
(mf/deps form)
(fn [items]
(let [value (str/join " " (map :text items))]
(fm/update-input-value! form input-name value))))
on-key-down
(mf/use-fn
(mf/deps @value)
(fn [event]
(let [val (cond-> @value trim str/trim)]
(cond
(or (kbd/enter? event) (kbd/comma? event) (kbd/space? event))
(do
(dom/prevent-default event)
(dom/stop-propagation event)
;; Once enter/comma is pressed we mark it as touched
(swap! form assoc-in [:touched input-name] true)
;; Empty values means "submit" the form (whent some items have been added
(when (and (kbd/enter? event) (str/empty? @value) (not-empty @items))
(when (fn? on-submit)
(on-submit form event)))
;; If we have a string in the input we add it only if valid
(when (and (valid-item-fn val) (not (str/empty? @value)))
(reset! value "")
;; Once added the form is back as "untouched"
(swap! form assoc-in [:touched input-name] false)
;; This split will allow users to copy comma/space separated values of emails
(doseq [val (str/split val #",|\s+")]
(swap! items conj-dedup {:text (str/trim val)
:valid (valid-item-fn val)
:caution (caution-item-fn val)}))))
(and (kbd/backspace? event) (str/empty? @value))
(do
(dom/prevent-default event)
(dom/stop-propagation event)
(swap! items (fn [items] (if (c/empty? items) items (pop items)))))))))
on-paste
(mf/use-fn
(fn [event]
(when-let [clipboard-data (.-clipboardData event)]
(let [paste-data (.getData clipboard-data "text")]
(when (and (string? paste-data)
(re-find #"[,\s]" paste-data))
(dom/prevent-default event)
(dom/stop-propagation event)
;; Mark as touched
(swap! form assoc-in [:touched input-name] true)
;; Split pasted text by commas and/or whitespace, add each valid part
(let [parts (->> (str/split paste-data #",|\s+")
(map str/trim)
(remove str/empty?))]
(doseq [part parts]
(when (valid-item-fn part)
(swap! items conj-dedup {:text part
:valid true
:caution (caution-item-fn part)})))
;; Reset input value and mark as untouched after successful paste
(reset! value "")
(swap! form assoc-in [:touched input-name] false)))))))
on-blur
(mf/use-fn
(fn [_]
(reset! focus? false)
(when-not (get-in @form [:touched input-name])
(swap! form assoc-in [:touched input-name] true))))
remove-item!
(mf/use-fn
(fn [item]
(swap! items #(into [] (remove (fn [x] (= x item))) %))))
manage-key-down
(mf/use-fn
(fn [item event]
(when (kbd/enter? event)
(remove-item! item))))]
(mf/with-effect [result @value]
(let [val (cond-> @value trim str/trim)
values (conj-dedup result {:text val :valid (valid-item-fn val)})
values (filterv #(:valid %) values)]
(update-form! values)))
[:div {:class klass}
[:input {:id (name input-name)
:name (name input-name)
:class in-klass
:type "text"
:auto-focus auto-focus?
:on-focus on-focus
:on-blur on-blur
:on-key-down on-key-down
:on-paste on-paste
:value @value
:on-change on-change
:placeholder (when empty? label)}]
[:label {:for (name input-name)} label]
(when-let [items (seq @items)]
[:div {:class (stl/css :selected-items)}
(for [item items]
[:div {:class (stl/css :selected-item)
:key (:text item)
:tab-index "0"
:on-key-down (partial manage-key-down item)}
[:span {:class (stl/css-case :around true
:invalid (not (:valid item))
:caution (:caution item))}
[:span {:class (stl/css :text)} (:text item)]
[:button {:class (stl/css :icon)
:on-click #(remove-item! item)} deprecated-icon/close]]])])]))

View File

@ -117,14 +117,15 @@
on-paste
(mf/use-fn
(fn [event]
(let [paste-data (-> event .-clipboardData (.getData "text"))]
(when (and (string? paste-data)
(re-find #"[,\s]" paste-data))
(dom/prevent-default event)
(dom/stop-propagation event)
(when-let [clipboard-data (.-clipboardData event)]
(let [paste-data (.getData clipboard-data "text")]
(when (and (string? paste-data)
(re-find #"[,\s]" paste-data))
(dom/prevent-default event)
(dom/stop-propagation event)
;; Mark as touched
(swap! form assoc-in [:touched name] true)
;; Mark as touched
(swap! form assoc-in [:touched name] true)
;; Split pasted text by commas and/or whitespace, add each valid part
(let [parts (->> (str/split paste-data #",|\s+")

View File

@ -47,7 +47,6 @@
[app.main.ui.workspace.webgl-unavailable-modal]
[app.util.debug :as dbg]
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.i18n :as i18n :refer [tr]]
[goog.events :as events]
[okulary.core :as l]
@ -180,7 +179,7 @@
(mf/with-effect []
(let [focus-out #(st/emit! (dw/workspace-focus-lost))
key (events/listen globals/window "blur" focus-out)]
key (events/listen js/window "blur" focus-out)]
(partial events/unlistenByKey key)))
(mf/with-effect [file-id page-id]
@ -262,7 +261,7 @@
(let [handle-wasm-render
(fn [_]
(reset! first-frame-rendered? true))
listener-key (events/listen globals/document "penpot:wasm:render" handle-wasm-render)]
listener-key (events/listen js/document "penpot:wasm:render" handle-wasm-render)]
(fn []
(events/unlistenByKey listener-key))))

View File

@ -55,7 +55,7 @@
nil)))
(defn- styles-fn [shape styles content]
(let [data (if (= (.getText ^js content) "")
(let [data (if (and content (= (.getText ^js content) ""))
(-> ^js (.getData content)
(.toJS)
(js->clj :keywordize-keys true))

View File

@ -7,62 +7,75 @@
(ns app.main.ui.workspace.viewport.viewport-ref
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.util.dom :as dom]
[app.util.mouse :as mse]
[goog.events :as events]
[rumext.v2 :as mf])
(:import goog.events.EventType))
[rumext.v2 :as mf]))
(defonce viewport-ref (atom nil))
(defonce current-observer (atom nil))
(defonce viewport-brect (atom nil))
(defn init-observer
[node on-change-bounds]
(defn- init-observer
[node]
(let [on-change-bounds
(fn [_]
(let [brect (dom/get-bounding-rect node)
brect (gpt/point (d/parse-integer (:left brect))
(d/parse-integer (:top brect)))]
(reset! viewport-brect brect)))
(let [observer (js/ResizeObserver. on-change-bounds)]
(when (some? @current-observer)
(.disconnect @current-observer))
observer
(js/ResizeObserver. on-change-bounds)]
(reset! current-observer observer)
(when (some? node)
(.observe observer node))))
(defn on-change-bounds
[_]
(when @viewport-ref
(let [brect (dom/get-bounding-rect @viewport-ref)
brect (gpt/point (d/parse-integer (:left brect))
(d/parse-integer (:top brect)))]
(reset! viewport-brect brect))))
(.observe observer node)
observer))
(defn create-viewport-ref
[]
(let [ref (mf/use-ref nil)]
[ref
(mf/use-memo
#(fn [node]
(mf/set-ref-val! ref node)
(reset! viewport-ref node)
(when (some? node)
(events/listen node EventType.MOUSELEAVE (fn [] (st/emit! (mse/->BlurEvent)))))
(init-observer node on-change-bounds)))]))
(let [node-ref (mf/use-ref nil)
handler-ref (mf/use-ref nil)
observer-ref (mf/use-ref nil)
callback (mf/use-fn
(fn [node]
;; Dispose all previous resources
(when-let [observer (mf/ref-val observer-ref)]
(.disconnect ^js observer)
(mf/set-ref-val! observer-ref nil))
(when-let [handler (mf/ref-val handler-ref)]
(when-let [node (mf/ref-val node-ref)]
(.removeEventListener ^js node "mouseleave" handler)
(mf/set-ref-val! handler-ref nil)))
;; Reset the ref values to the current node (can be nil)
(mf/set-ref-val! node-ref node)
(reset! viewport-ref node)
(when (some? node)
(let [handler (fn [] (st/emit! (mse/->BlurEvent)))
observer (init-observer node)]
(.addEventListener ^js node "mouseleave" handler)
(mf/set-ref-val! handler-ref handler)
(mf/set-ref-val! observer-ref observer)))))]
[node-ref callback]))
(defn point->viewport
[pt]
(let [zoom (dm/get-in @st/state [:workspace-local :zoom] 1)]
(when (and (some? @viewport-ref)
(some? @viewport-brect))
(let [vbox (.. ^js @viewport-ref -viewBox -baseVal)
brect @viewport-brect
box (gpt/point (.-x vbox) (.-y vbox))
zoom (gpt/point zoom)]
(let [zoom (d/nilv @refs/selected-zoom 1)
viewport-node @viewport-ref
viewport-brect @viewport-brect]
(-> (gpt/subtract pt brect)
(when (and (some? viewport-brect)
(some? viewport-node))
(let [vbox (.. ^js viewport-node -viewBox -baseVal)
box (gpt/point (.-x vbox) (.-y vbox))
zoom (gpt/point zoom)]
(-> (gpt/subtract pt viewport-brect)
(gpt/divide zoom)
(gpt/add box))))))
@ -71,8 +84,8 @@
Unlike point->viewport, this does NOT convert to canvas coordinates -
it just subtracts the viewport's bounding rect offset."
[pt]
(when (some? @viewport-brect)
(gpt/subtract pt @viewport-brect)))
(when-let [brect @viewport-brect]
(gpt/subtract pt brect)))
(defn inside-viewport?
[target]

View File

@ -115,13 +115,13 @@
([e]
(get-data e "penpot/data"))
([e data-type]
(let [dt (.-dataTransfer e)
data (.getData dt data-type)]
(cond-> data
(and (some? data) (not= data "")
(or (str/starts-with? data-type "penpot")
(= data-type "application/json")))
(t/decode-str)))))
(when-let [dt (.-dataTransfer e)]
(let [data (.getData dt data-type)]
(cond-> data
(and (some? data) (not= data "")
(or (str/starts-with? data-type "penpot")
(= data-type "application/json")))
(t/decode-str))))))
(defn get-files
[e]

View File

@ -60,12 +60,14 @@
(defn get-editor-block-data
[block]
(-> (.getData ^js block)
(immutable-map->map)))
(when (some? block)
(-> (.getData ^js block)
(immutable-map->map))))
(defn get-editor-block-type
[block]
(.getType ^js block))
(when (some? block)
(.getType ^js block)))
(defn get-editor-current-block-data
[state]

View File

@ -82,6 +82,10 @@
(defn read-as-text
[entry]
(when (nil? entry)
(ex/raise :type :assertion
:code :invalid-entry
:hint "cannot read zip entry: entry is nil"))
(let [writer (new zip/TextWriter)]
(.getData entry writer)))

View File

@ -7,6 +7,7 @@
(ns app.worker.import
(:refer-clojure :exclude [resolve])
(:require
[app.common.exceptions :as ex]
[app.common.json :as json]
[app.common.logging :as log]
[app.common.schema :as sm]
@ -44,10 +45,15 @@
(def conjv (fnil conj []))
(defn- read-zip-manifest
(defn read-zip-manifest
[zip-reader]
(->> (rx/from (uz/get-entry zip-reader "manifest.json"))
(rx/mapcat uz/read-as-text)
(rx/mapcat (fn [entry]
(if (nil? entry)
(rx/throw (ex/error :type :validation
:code :invalid-penpot-file
:hint "Not a valid Penpot file: manifest.json is missing"))
(uz/read-as-text entry))))
(rx/map json/decode)))
(defn slurp-uri

View File

@ -0,0 +1,49 @@
;; 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.data.store-test
"Unit tests for app.main.store.
Tests cover:
- format-last-events empty, single, multi, column alignment"
(:require
[app.main.store :as st]
[cljs.test :as t :include-macros true]
[cuerdas.core :as str]))
(t/deftest format-last-events-empty
(t/testing "empty events produce empty string"
(t/is (= "" (st/format-last-events [])))))
(t/deftest format-last-events-single
(t/testing "a single event shows (+0ms) and its name"
(let [result (st/format-last-events [{:name ":test/event" :t (js/Date. 1000)}])]
(t/is (str/includes? result "(+0ms)"))
(t/is (str/includes? result ":test/event"))
(t/is (= 1 (count (str/split result "\n")))))))
(t/deftest format-last-events-multi
(t/testing "multiple events show correct deltas and event names"
(let [events [{:name ":event/a" :t (js/Date. 0)}
{:name ":event/b" :t (js/Date. 500)}
{:name ":event/c" :t (js/Date. 2500)}]
lines (str/split (st/format-last-events events) "\n")]
(t/is (= 3 (count lines)))
(t/is (some #(str/includes? % ":event/a") lines))
(t/is (some #(str/includes? % ":event/b") lines))
(t/is (some #(str/includes? % ":event/c") lines))
(t/is (str/includes? (nth lines 0) "(+0ms)"))
(t/is (str/includes? (nth lines 1) "(+500ms)"))
(t/is (str/includes? (nth lines 2) "(+2000ms)")))))
(t/deftest format-last-events-alignment
(t/testing "event names start at the same column across all lines"
(let [events [{:name ":evt-a" :t (js/Date. 0)}
{:name ":evt-b" :t (js/Date. 500)}]
lines (str/split (st/format-last-events events) "\n")
col-a (.indexOf (nth lines 0) ":evt-a")
col-b (.indexOf (nth lines 1) ":evt-b")]
(t/is (pos? col-a))
(t/is (= col-a col-b)))))

View File

@ -10,6 +10,7 @@
[frontend-tests.copy-as-svg-test]
[frontend-tests.data.nitrate-test]
[frontend-tests.data.repo-test]
[frontend-tests.data.store-test]
[frontend-tests.data.uploads-test]
[frontend-tests.data.viewer-test]
[frontend-tests.data.workspace-colors-test]
@ -48,6 +49,7 @@
[frontend-tests.plugins.value-objects-test]
[frontend-tests.render-wasm.process-objects-test]
[frontend-tests.svg-fills-test]
[frontend-tests.text-editor-paste-guard-test]
[frontend-tests.tokens.copy-paste-props-test]
[frontend-tests.tokens.import-export-test]
[frontend-tests.tokens.logic.token-actions-test]
@ -69,7 +71,10 @@
[frontend-tests.util-object-test]
[frontend-tests.util-range-tree-test]
[frontend-tests.util-simple-math-test]
[frontend-tests.util-text-editor-test]
[frontend-tests.util-webapi-test]
[frontend-tests.util-zip-test]
[frontend-tests.util.dom.dnd-test]
[frontend-tests.worker-snap-test]
[goog.object :as gobj]))
@ -91,6 +96,7 @@
'frontend-tests.copy-as-svg-test
'frontend-tests.data.nitrate-test
'frontend-tests.data.repo-test
'frontend-tests.data.uploads-test
'frontend-tests.data.viewer-test
'frontend-tests.data.workspace-colors-test
@ -144,13 +150,17 @@
'frontend-tests.ui.gradient-handlers-test
'frontend-tests.ui.layout-container-multiple-test
'frontend-tests.ui.measures-menu-props-test
'frontend-tests.text-editor-paste-guard-test
'frontend-tests.ui.settings-password-schema-test
'frontend-tests.ui.settings-shortcuts-test
'frontend-tests.util-clipboard-test
'frontend-tests.util-object-test
'frontend-tests.util-range-tree-test
'frontend-tests.util-simple-math-test
'frontend-tests.util-text-editor-test
'frontend-tests.util-webapi-test
'frontend-tests.util.dom.dnd-test
'frontend-tests.util-zip-test
'frontend-tests.worker-snap-test])
(assert (every? find-ns-obj test-namespaces)

View File

@ -0,0 +1,62 @@
;; 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.text-editor-paste-guard-test
"Regression tests for the Cannot read properties of undefined
(reading getData) family of bugs. Each test verifies that the inner
getData call is now guarded by an outer when-let on clipboardData
so that a synthetic event with no clipboardData no longer throws."
(:require
[cljs.test :as t :include-macros true]))
(defn- guarded-get-data-text
"Mirrors the body of the fixed paste handlers in main/ui/forms.cljs and
main/ui/components/forms.cljs."
[event]
(when-let [clipboard-data (.-clipboardData event)]
(.getData clipboard-data "text")))
(defn- guarded-get-data-text-plain
"Mirrors the body of the fixed paste handler in
main/ui/workspace/shapes/text/v3_editor.cljs."
[event]
(when-let [clipboard-data (.-clipboardData event)]
(.getData clipboard-data "text/plain")))
(t/deftest guarded-paste-handlers-do-not-throw-on-missing-clipboardData
(t/testing "event without clipboardData returns nil (no throw)"
(t/is (nil? (guarded-get-data-text #js {})))
(t/is (nil? (guarded-get-data-text-plain #js {}))))
(t/testing "event with explicit nil clipboardData returns nil"
(t/is (nil? (guarded-get-data-text #js {:clipboardData nil})))
(t/is (nil? (guarded-get-data-text-plain #js {:clipboardData nil}))))
(t/testing "event with valid clipboardData returns the text"
(let [text-cb (fn [t] (if (= t "text") "hello" nil))
text-plain-cb (fn [t] (if (= t "text/plain") "hello" nil))
cb #js {:getData (fn [t] (if (= t "text") "hello" nil))}
cbp #js {:getData (fn [t] (if (= t "text/plain") "hello" nil))}]
(t/is (= "hello" (guarded-get-data-text #js {:clipboardData cb})))
(t/is (= "hello" (guarded-get-data-text-plain #js {:clipboardData cbp}))))))
;; Mirrors the fixed styles-fn body in main/ui/workspace/shapes/text/editor.cljs.
;; The fix adds an (and content ...) guard so getText and getData are never
;; called on a nil content object.
(defn- guarded-styles-fn-branch
"Returns the data that styles-fn would use for a given content. When content
is nil, the function falls back to the styles-only branch and returns :fallback
(the real function calls legacy.txt/styles-to-attrs which we don't exercise
here — we only verify the guard itself prevents the getText/getData throws)."
[content]
(if (and content (= (.getText ^js content) ""))
(-> ^js (.getData content)
(.toJS)
(js->clj :keywordize-keys true))
:fallback))
(t/deftest guarded-styles-fn-branch-does-not-throw-on-nil-content
(t/testing "nil content falls back to the styles branch (no throw)"
(t/is (= :fallback (guarded-styles-fn-branch nil)))
(t/is (= :fallback (guarded-styles-fn-branch js/undefined)))))

View File

@ -0,0 +1,23 @@
;; 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.util.dom.dnd-test
(:require
[app.util.dom.dnd :as dnd]
[cljs.test :as t :include-macros true]))
(t/deftest get-data-returns-nil-when-event-has-no-dataTransfer
(t/testing "event without dataTransfer"
(t/is (nil? (dnd/get-data #js {}))))
(t/testing "event with explicit nil dataTransfer"
(t/is (nil? (dnd/get-data #js {:dataTransfer nil}))))
(t/testing "explicit data-type also returns nil for missing dataTransfer"
(t/is (nil? (dnd/get-data #js {} "penpot/data")))))
(t/deftest get-data-reads-from-dataTransfer
(t/testing "dataTransfer with matching key returns the value (non-decoded type)"
(let [dt #js {:getData (fn [_type] "hello")}]
(t/is (= "hello" (dnd/get-data #js {:dataTransfer dt} "text/plain"))))))

View File

@ -0,0 +1,18 @@
;; 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.util-text-editor-test
(:require
[app.util.text-editor :as te]
[cljs.test :as t :include-macros true]))
(t/deftest get-editor-block-data-returns-nil-for-nil-block
(t/is (nil? (te/get-editor-block-data nil)))
(t/is (nil? (te/get-editor-block-data js/undefined))))
(t/deftest get-editor-block-type-returns-nil-for-nil-block
(t/is (nil? (te/get-editor-block-type nil)))
(t/is (nil? (te/get-editor-block-type js/undefined))))

View File

@ -0,0 +1,44 @@
;; 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.util-zip-test
(:require
[app.util.zip :as-alias uz]
[app.worker.import :as worker.import]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]
[promesa.core :as p]))
(t/deftest read-as-text-nil-entry-raises-typed-error
(t/testing "read-as-text guards against nil entry"
(t/is (thrown-with-msg? js/Error #"nil"
(app.util.zip/read-as-text nil)))))
(t/deftest read-zip-manifest-missing-throws-validation-error
(t/async
done
(t/testing "read-zip-manifest rejects ZIPs without manifest.json"
(mock/with-mocks
{app.util.zip/get-entry (mock/stub (fn [_ _] (p/resolved nil)))}
(fn [done']
(->> (worker.import/read-zip-manifest #js {})
(rx/subs!
(fn [_]
(t/is false "expected validation error to be thrown")
(done'))
(fn [err]
(let [data (ex-data err)]
(t/is (= :invalid-penpot-file (:code data))
"missing manifest.json raises typed :invalid-penpot-file error")
(t/is (string? (:hint data))
"missing manifest.json error carries a :hint")
(t/is (re-find #"manifest\.json" (:hint data))
"missing manifest.json :hint mentions manifest.json")
(t/is (nil? (re-find #"getData" (:hint data)))
"missing manifest.json :hint does not leak the raw TypeError text")
(done'))))))
done))))