diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index fba1728d8c..7f21245f49 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -1048,7 +1048,15 @@ (when-let [color (-> state (select-keys [:image :gradient :color :opacity]) (not-empty))] - (rx/of (add-recent-color color)))))))) + ;; Closing the dialog while an image-fill upload is still in + ;; flight (or a gradient is mid-edit) leaves the colorpicker + ;; with a partial selection — opacity-only, or with stops not + ;; yet committed. ``add-recent-color`` runs the value through + ;; ``check-color`` and asserts; gate on the same schema here + ;; so the partial value is silently dropped instead of crashing + ;; the workspace. + (when (clr/valid-color? color) + (rx/of (add-recent-color color))))))))) (defn update-colorpicker-gradient [changes] diff --git a/frontend/test/frontend_tests/data/workspace_colors_test.cljs b/frontend/test/frontend_tests/data/workspace_colors_test.cljs index 947bb6e820..7b2006eb22 100644 --- a/frontend/test/frontend_tests/data/workspace_colors_test.cljs +++ b/frontend/test/frontend_tests/data/workspace_colors_test.cljs @@ -46,3 +46,39 @@ (ptk/event? (dwc/add-fill #{uuid/zero} color3 1)))) (t/is (thrown? js/Error (ptk/event? (dwc/add-fill #{uuid/zero} color4 1)))))) + + +(t/deftest update-colorpicker-color-skips-add-recent-on-incomplete-image-state + ;; Regression for https://github.com/penpot/penpot/issues/8443. + ;; + ;; Closing the fill dialog while the image upload is still in flight leaves + ;; the colorpicker's current-color with only :opacity (no :image, :gradient, + ;; or :color). Before the guard, ptk/watch eagerly built (add-recent-color + ;; partial), which calls (clr/check-color partial) and threw an "expected + ;; valid color" assertion that surfaced as an Internal Assertion Error toast. + (let [partial-image-state {:colorpicker {:type :image + :current-color {:opacity 1}}} + event (dwc/update-colorpicker-color {} true)] + (t/is (nil? (ptk/watch event partial-image-state nil))))) + + +(t/deftest update-colorpicker-color-skips-add-recent-when-only-opacity-on-color-type + ;; Same incomplete-state shape, but the colorpicker is on the plain-color tab + ;; (e.g. the user clicked elsewhere before the picker had a chance to commit + ;; a hex). The existing :type-and-:color-nil guard sits on the colorpicker + ;; map's :type — but get-color-from-colorpicker-state strips :type from its + ;; output, so that guard never fires. The schema-based guard catches it. + (let [colorless-state {:colorpicker {:type :color + :current-color {:opacity 1}}} + event (dwc/update-colorpicker-color {} true)] + (t/is (nil? (ptk/watch event colorless-state nil))))) + + +(t/deftest update-colorpicker-color-still-emits-recent-for-valid-plain-color + ;; Sanity check: a fully-populated plain color still produces a watch + ;; observable (the rx/of branch is reached) so we know the guard isn't + ;; over-eager and silently dropping legitimate colors. + (let [valid-state {:colorpicker {:type :color + :current-color {:color "#ff0000" :opacity 1}}} + event (dwc/update-colorpicker-color {} true)] + (t/is (some? (ptk/watch event valid-state nil)))))