mirror of
https://github.com/penpot/penpot.git
synced 2026-10-01 00:06:17 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
f9b8f1ba75
@ -1256,10 +1256,18 @@
|
|||||||
|
|
||||||
no-bool-shapes? (->> all-selected (some (comp #{:frame :text} :type)))]
|
no-bool-shapes? (->> all-selected (some (comp #{:frame :text} :type)))]
|
||||||
|
|
||||||
(if (and (some? shape) (not (contains? selected (:id shape))))
|
(cond
|
||||||
|
;; A shape missing from the current page can never become
|
||||||
|
;; selected, so selecting it and retrying would recurse forever
|
||||||
|
(and (some? shape) (not (contains? objects (:id shape))))
|
||||||
|
(rx/empty)
|
||||||
|
|
||||||
|
(and (some? shape) (not (contains? selected (:id shape))))
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(rx/of (dws/select-shape (:id shape)))
|
(rx/of (dws/select-shape (:id shape)))
|
||||||
(rx/of (show-shape-context-menu params)))
|
(rx/of (show-shape-context-menu params)))
|
||||||
|
|
||||||
|
:else
|
||||||
(rx/of (show-context-menu
|
(rx/of (show-context-menu
|
||||||
(-> params
|
(-> params
|
||||||
(assoc
|
(assoc
|
||||||
|
|||||||
@ -877,13 +877,58 @@
|
|||||||
(and (string? stack)
|
(and (string? stack)
|
||||||
(str/includes? stack "posthog"))))
|
(str/includes? stack "posthog"))))
|
||||||
|
|
||||||
|
;; Captures the script location of a stack frame: the part preceding
|
||||||
|
;; ":line:column", in both the V8 ("at fn (location:1:2)") and the
|
||||||
|
;; SpiderMonkey/JSC ("fn@location:1:2") formats.
|
||||||
|
(def ^:private frame-location-rx
|
||||||
|
#"(?:at |[@(])([^\s()]+):\d+:\d+")
|
||||||
|
|
||||||
|
(defn- stack-frames
|
||||||
|
"The frame lines of a stack trace, without the leading message line
|
||||||
|
that V8 puts before them."
|
||||||
|
[stack]
|
||||||
|
(into []
|
||||||
|
(comp (map str/trim)
|
||||||
|
(filter (fn [line]
|
||||||
|
(or (str/starts-with? line "at ")
|
||||||
|
(str/includes? line "@")))))
|
||||||
|
(str/lines stack)))
|
||||||
|
|
||||||
|
(defn- located-frame?
|
||||||
|
"True when a stack frame names the script it belongs to. Code injected
|
||||||
|
into the page carries no script to name and reports `<anonymous>`."
|
||||||
|
[frame]
|
||||||
|
(when-let [location (second (re-find frame-location-rx frame))]
|
||||||
|
(not (str/starts-with? location "<"))))
|
||||||
|
|
||||||
|
(defn- from-injected-code?
|
||||||
|
"True when the stack is blank, or has frames but none of them names a
|
||||||
|
script. Either way the error was raised by third-party code injected
|
||||||
|
into the page (an extension content script, a desktop wrapper, a
|
||||||
|
bookmarklet) rather than by application code. Firefox, for instance,
|
||||||
|
raises \"can't access dead object\" with a blank stack when an unloaded
|
||||||
|
extension's code runs.
|
||||||
|
|
||||||
|
Anything our own code calls keeps our frames further down the stack, so
|
||||||
|
a stack without a single named script never belongs to us and there is
|
||||||
|
nothing actionable on our side."
|
||||||
|
[cause]
|
||||||
|
(let [stack (.-stack cause)]
|
||||||
|
(and (string? stack)
|
||||||
|
(or (str/blank? stack)
|
||||||
|
(let [frames (stack-frames stack)]
|
||||||
|
(and (some? (seq frames))
|
||||||
|
(not (some located-frame? frames))))))))
|
||||||
|
|
||||||
(defn is-ignorable-exception?
|
(defn is-ignorable-exception?
|
||||||
"True when the error is known to be harmless (browser extensions, analytics,
|
"True when the error is known to be harmless (browser extensions, injected
|
||||||
React/extension DOM conflicts, etc.) and should NOT be surfaced to the user."
|
third-party code, analytics, React/extension DOM conflicts, etc.) and
|
||||||
|
should NOT be surfaced to the user."
|
||||||
[cause]
|
[cause]
|
||||||
(let [message (ex-message cause)]
|
(let [message (ex-message cause)]
|
||||||
(or (from-extension? cause)
|
(or (from-extension? cause)
|
||||||
(from-posthog? cause)
|
(from-posthog? cause)
|
||||||
|
(from-injected-code? cause)
|
||||||
(= message "Possible side-effect in debug-evaluate")
|
(= message "Possible side-effect in debug-evaluate")
|
||||||
(= message "Unexpected end of input")
|
(= message "Unexpected end of input")
|
||||||
(str/starts-with? message "invalid props on component")
|
(str/starts-with? message "invalid props on component")
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
;; 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.workspace-context-menu-test
|
||||||
|
(:require
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.data.workspace :as dw]
|
||||||
|
[app.main.data.workspace.selection :as dws]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
|
[cljs.test :as t :include-macros true]
|
||||||
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
|
(def ^:private file-id (uuid/next))
|
||||||
|
(def ^:private page-id (uuid/next))
|
||||||
|
(def ^:private rect-id (uuid/next))
|
||||||
|
|
||||||
|
(defn- make-state
|
||||||
|
[selected]
|
||||||
|
{:current-file-id file-id
|
||||||
|
:current-page-id page-id
|
||||||
|
:workspace-local {:selected selected}
|
||||||
|
:files {file-id
|
||||||
|
{:data
|
||||||
|
{:pages-index
|
||||||
|
{page-id
|
||||||
|
{:objects {uuid/zero {:id uuid/zero :type :frame :shapes [rect-id]}
|
||||||
|
rect-id {:id rect-id :type :rect :parent-id uuid/zero
|
||||||
|
:frame-id uuid/zero}}}}}}}})
|
||||||
|
|
||||||
|
(defn- emitted-types
|
||||||
|
"Collect the types of the events emitted by the event's watch."
|
||||||
|
[event state done f]
|
||||||
|
(let [types (atom [])]
|
||||||
|
(->> (ptk/watch event state (rx/empty))
|
||||||
|
(rx/subs!
|
||||||
|
#(swap! types conj (ptk/type %))
|
||||||
|
(fn [err]
|
||||||
|
(t/do-report {:type :error :message "Stream error" :actual err})
|
||||||
|
(done))
|
||||||
|
(fn []
|
||||||
|
(f @types)
|
||||||
|
(done))))))
|
||||||
|
|
||||||
|
(defn- menu-event
|
||||||
|
[shape-id]
|
||||||
|
(dw/show-shape-context-menu {:position (gpt/point 10 10)
|
||||||
|
:shape {:id shape-id :type :rect}}))
|
||||||
|
|
||||||
|
(t/deftest shape-context-menu-ignores-shape-outside-page
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(emitted-types (menu-event (uuid/next)) (make-state #{}) done
|
||||||
|
#(t/is (= [] %)))))
|
||||||
|
|
||||||
|
(t/deftest shape-context-menu-selects-unselected-shape
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(emitted-types (menu-event rect-id) (make-state #{}) done
|
||||||
|
#(t/is (= [::dws/select-shape ::dw/show-shape-context-menu] %)))))
|
||||||
|
|
||||||
|
(t/deftest shape-context-menu-opens-for-selected-shape
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(emitted-types (menu-event rect-id) (make-state #{rect-id}) done
|
||||||
|
#(t/is (= [::dw/show-context-menu] %)))))
|
||||||
@ -10,8 +10,9 @@
|
|||||||
[cljs.test :as t :include-macros true]))
|
[cljs.test :as t :include-macros true]))
|
||||||
|
|
||||||
(defn- make-error
|
(defn- make-error
|
||||||
"Create a JS Error-like object with the given name, message, and optional stack."
|
"Create a JS Error-like object with the given name, message, and optional
|
||||||
[error-name message & {:keys [stack] :or {stack ""}}]
|
stack. Without a stack, the error keeps the one the runtime gave it."
|
||||||
|
[error-name message & {:keys [stack]}]
|
||||||
(let [err (js/Error. message)]
|
(let [err (js/Error. message)]
|
||||||
(set! (.-name err) error-name)
|
(set! (.-name err) error-name)
|
||||||
(when (some? stack)
|
(when (some? stack)
|
||||||
@ -93,3 +94,57 @@
|
|||||||
(t/testing "Regular TypeError is NOT ignorable"
|
(t/testing "Regular TypeError is NOT ignorable"
|
||||||
(let [cause (make-error "TypeError" "undefined is not a function")]
|
(let [cause (make-error "TypeError" "undefined is not a function")]
|
||||||
(t/is (false? (errors/is-ignorable-exception? cause))))))
|
(t/is (false? (errors/is-ignorable-exception? cause))))))
|
||||||
|
|
||||||
|
(t/deftest test-ignorable-injected-script
|
||||||
|
(t/testing "Errors raised by code injected into the page are ignorable"
|
||||||
|
(let [cause (make-error "TypeError" "Cannot read properties of undefined (reading 'get')"
|
||||||
|
:stack (str "TypeError: Cannot read properties of undefined (reading 'get')\n"
|
||||||
|
" at nativeVis (<anonymous>:5:35)\n"
|
||||||
|
" at HTMLDocument.<anonymous> (<anonymous>:21:64)\n"
|
||||||
|
" at <anonymous>:17:33"))]
|
||||||
|
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||||
|
|
||||||
|
(t/deftest test-not-ignorable-injected-script-calling-into-app
|
||||||
|
(t/testing "Errors whose stack reaches application code are NOT ignorable"
|
||||||
|
(let [cause (make-error "TypeError" "Cannot read properties of undefined (reading 'get')"
|
||||||
|
:stack (str "TypeError: Cannot read properties of undefined (reading 'get')\n"
|
||||||
|
" at nativeVis (<anonymous>:5:35)\n"
|
||||||
|
" at zLe (https://design.penpot.app/js/main.js:42:13)"))]
|
||||||
|
(t/is (false? (errors/is-ignorable-exception? cause))))))
|
||||||
|
|
||||||
|
(t/deftest test-not-ignorable-release-build-v8-stack
|
||||||
|
(t/testing "Application errors from a release build are NOT ignorable (V8 stack)"
|
||||||
|
;; Frames as they appear in production: advanced-compiled names, an
|
||||||
|
;; asset URL carrying the version query string, and a trailing frame
|
||||||
|
;; without a function name.
|
||||||
|
(let [cause (make-error "TypeError" "Cannot read properties of null (reading 'toString')"
|
||||||
|
:stack (str "TypeError: Cannot read properties of null (reading 'toString')\n"
|
||||||
|
" at $app$main$ui$workspace$shapes$path$editor$path_editor_STAR_$$ "
|
||||||
|
"(https://design.penpot.app/js/main-workspace.js?version=2.18.0-RC5-1789051786:8580:99)\n"
|
||||||
|
" at $re (https://design.penpot.app/js/libs.js?version=2.18.0-RC5-1789051786:150:48345)\n"
|
||||||
|
" at https://design.penpot.app/js/libs.js?version=2.18.0-RC5-1789051786:150:125675"))]
|
||||||
|
(t/is (false? (errors/is-ignorable-exception? cause))))))
|
||||||
|
|
||||||
|
(t/deftest test-not-ignorable-release-build-spidermonkey-stack
|
||||||
|
(t/testing "Application errors from a release build are NOT ignorable (SpiderMonkey/JSC stack)"
|
||||||
|
;; SpiderMonkey stacks carry no message line and name the script after
|
||||||
|
;; an @ instead of wrapping it in parentheses.
|
||||||
|
(let [cause (make-error "InternalError" "too much recursion"
|
||||||
|
:stack (str "$APP.$JSCompiler_prototypeAlias$$.$inode_lookup$"
|
||||||
|
"@https://design.penpot.app/js/shared.js?version=2.18.0-RC5-1789051786:23076:58\n"
|
||||||
|
"$app$common$types$container$get_component_shape$cljs$0core$0IFn$0_invoke$0arity$03$$"
|
||||||
|
"@https://design.penpot.app/js/shared.js?version=2.18.0-RC5-1789051786:7596:1"))]
|
||||||
|
(t/is (false? (errors/is-ignorable-exception? cause))))))
|
||||||
|
|
||||||
|
(t/deftest test-ignorable-blank-stack
|
||||||
|
(t/testing "Errors with a blank stack are ignorable"
|
||||||
|
;; Firefox raises "can't access dead object" with an empty stack when
|
||||||
|
;; code from an unloaded extension runs in the page.
|
||||||
|
(let [cause (make-error "TypeError" "can't access dead object" :stack "")]
|
||||||
|
(t/is (true? (errors/is-ignorable-exception? cause))))))
|
||||||
|
|
||||||
|
(t/deftest test-not-ignorable-message-only-stack
|
||||||
|
(t/testing "Errors whose stack holds only the message line are NOT ignorable"
|
||||||
|
;; V8 gives native rejections such as a failed fetch no frames at all.
|
||||||
|
(let [cause (make-error "TypeError" "Failed to fetch" :stack "TypeError: Failed to fetch")]
|
||||||
|
(t/is (false? (errors/is-ignorable-exception? cause))))))
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
[frontend-tests.data.wasm-text-test]
|
[frontend-tests.data.wasm-text-test]
|
||||||
[frontend-tests.data.workspace-colors-test]
|
[frontend-tests.data.workspace-colors-test]
|
||||||
[frontend-tests.data.workspace-comments-test]
|
[frontend-tests.data.workspace-comments-test]
|
||||||
|
[frontend-tests.data.workspace-context-menu-test]
|
||||||
[frontend-tests.data.workspace-interactions-test]
|
[frontend-tests.data.workspace-interactions-test]
|
||||||
[frontend-tests.data.workspace-mcp-test]
|
[frontend-tests.data.workspace-mcp-test]
|
||||||
[frontend-tests.data.workspace-media-test]
|
[frontend-tests.data.workspace-media-test]
|
||||||
@ -157,6 +158,7 @@
|
|||||||
'frontend-tests.data.wasm-text-test
|
'frontend-tests.data.wasm-text-test
|
||||||
'frontend-tests.data.workspace-colors-test
|
'frontend-tests.data.workspace-colors-test
|
||||||
'frontend-tests.data.workspace-comments-test
|
'frontend-tests.data.workspace-comments-test
|
||||||
|
'frontend-tests.data.workspace-context-menu-test
|
||||||
'frontend-tests.data.workspace-interactions-test
|
'frontend-tests.data.workspace-interactions-test
|
||||||
'frontend-tests.data.workspace-mcp-test
|
'frontend-tests.data.workspace-mcp-test
|
||||||
'frontend-tests.data.workspace-media-test
|
'frontend-tests.data.workspace-media-test
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user