mirror of
https://github.com/penpot/penpot.git
synced 2026-07-29 09:26:14 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
535ccfb930
@ -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();
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))))
|
||||
|
||||
|
||||
@ -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]]])])]))
|
||||
|
||||
@ -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+")
|
||||
|
||||
@ -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))))
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)))
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
49
frontend/test/frontend_tests/data/store_test.cljs
Normal file
49
frontend/test/frontend_tests/data/store_test.cljs
Normal 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)))))
|
||||
@ -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)
|
||||
|
||||
@ -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)))))
|
||||
23
frontend/test/frontend_tests/util/dom/dnd_test.cljs
Normal file
23
frontend/test/frontend_tests/util/dom/dnd_test.cljs
Normal 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"))))))
|
||||
18
frontend/test/frontend_tests/util_text_editor_test.cljs
Normal file
18
frontend/test/frontend_tests/util_text_editor_test.cljs
Normal 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))))
|
||||
44
frontend/test/frontend_tests/util_zip_test.cljs
Normal file
44
frontend/test/frontend_tests/util_zip_test.cljs
Normal 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))))
|
||||
Loading…
x
Reference in New Issue
Block a user