Slide button component (bounty) (#16259)
This commit is contained in:
parent
4f4489ee51
commit
d43b73b566
|
@ -0,0 +1,101 @@
|
|||
(ns quo2.components.buttons.slide-button.animations
|
||||
(:require
|
||||
[react-native.gesture :as gesture]
|
||||
[quo2.components.buttons.slide-button.utils :as utils]
|
||||
[oops.core :as oops]
|
||||
[react-native.reanimated :as reanimated]))
|
||||
|
||||
(def ^:private extrapolation
|
||||
{:extrapolateLeft "clamp"
|
||||
:extrapolateRight "clamp"})
|
||||
|
||||
(defn- track-interpolation-inputs
|
||||
[in-vectors track-width]
|
||||
(map (partial * track-width) in-vectors))
|
||||
|
||||
;; Interpolations
|
||||
(defn- track-clamp-interpolation
|
||||
[track-width]
|
||||
{:in [-1 0 1]
|
||||
:out [track-width 0 track-width]})
|
||||
|
||||
(defn- track-cover-interpolation
|
||||
[track-width thumb-size]
|
||||
{:in [0 1]
|
||||
:out [(/ thumb-size 2) track-width]})
|
||||
|
||||
(defn- arrow-icon-position-interpolation
|
||||
[thumb-size]
|
||||
{:in [0.9 1]
|
||||
:out [0 (- thumb-size)]})
|
||||
|
||||
(defn- action-icon-position-interpolation
|
||||
[thumb-size]
|
||||
{:in [0.9 1]
|
||||
:out [thumb-size 0]})
|
||||
|
||||
(defn interpolate-track
|
||||
"Interpolate the position in the track
|
||||
`x-pos` Track animated value
|
||||
`track-width` Usable width of the track
|
||||
`thumb-size` Size of the thumb
|
||||
`interpolation` ` :thumb-border-radius`/`:thumb-drop-position`/`:thumb-drop-scale`/`:thumb-drop-z-index`/..."
|
||||
([x-pos track-width thumb-size interpolation]
|
||||
(let [interpolations {:track-cover (track-cover-interpolation track-width thumb-size)
|
||||
:track-clamp (track-clamp-interpolation track-width)
|
||||
:action-icon-position (action-icon-position-interpolation thumb-size)
|
||||
:arrow-icon-position (arrow-icon-position-interpolation thumb-size)}
|
||||
|
||||
interpolation-values (interpolation interpolations)
|
||||
output (:out interpolation-values)
|
||||
input (-> (:in interpolation-values)
|
||||
(track-interpolation-inputs track-width))]
|
||||
(if interpolation-values
|
||||
(reanimated/interpolate x-pos
|
||||
input
|
||||
output
|
||||
extrapolation)
|
||||
x-pos))))
|
||||
|
||||
;; Animations
|
||||
(defn- animate-spring
|
||||
[value to-value]
|
||||
(reanimated/animate-shared-value-with-spring value
|
||||
to-value
|
||||
{:mass 1
|
||||
:damping 30
|
||||
:stiffness 400}))
|
||||
|
||||
(defn- complete-animation
|
||||
[sliding-complete?]
|
||||
(reset! sliding-complete? true))
|
||||
|
||||
(defn- reset-track-position
|
||||
[x-pos]
|
||||
(animate-spring x-pos 0))
|
||||
|
||||
;; Gestures
|
||||
(defn drag-gesture
|
||||
[x-pos
|
||||
gestures-disabled?
|
||||
disabled?
|
||||
track-width
|
||||
sliding-complete?]
|
||||
(let [gestures-enabled? (not (or disabled? @gestures-disabled?))]
|
||||
(-> (gesture/gesture-pan)
|
||||
(gesture/with-test-ID :slide-button-gestures)
|
||||
(gesture/enabled gestures-enabled?)
|
||||
(gesture/min-distance 0)
|
||||
(gesture/on-update (fn [event]
|
||||
(let [x-translation (oops/oget event "translationX")
|
||||
clamped-x (utils/clamp-value x-translation 0 track-width)
|
||||
reached-end? (>= clamped-x track-width)]
|
||||
(reanimated/set-shared-value x-pos clamped-x)
|
||||
(when (and reached-end? (not @sliding-complete?))
|
||||
(reset! gestures-disabled? true)
|
||||
(complete-animation sliding-complete?)))))
|
||||
(gesture/on-end (fn [event]
|
||||
(let [x-translation (oops/oget event "translationX")
|
||||
reached-end? (>= x-translation track-width)]
|
||||
(when (not reached-end?)
|
||||
(reset-track-position x-pos))))))))
|
|
@ -0,0 +1,105 @@
|
|||
(ns quo2.components.buttons.slide-button.component-spec
|
||||
(:require [quo2.components.buttons.slide-button.view :as slide-button]
|
||||
[quo2.components.buttons.slide-button.constants :as constants]
|
||||
[quo2.components.buttons.slide-button.utils :as utils]
|
||||
["@testing-library/react-native" :as rtl]
|
||||
["react-native-gesture-handler/jest-utils" :as gestures-jest]
|
||||
[reagent.core :as r]
|
||||
[test-helpers.component :as h]))
|
||||
|
||||
;; NOTE stolen from
|
||||
;; (https://github.com/reagent-project/reagent/blob/a14faba55e373000f8f93edfcfce0d1222f7e71a/test/reagenttest/utils.cljs#LL104C7-L104C10),
|
||||
;;
|
||||
;; There's also a comment over there about it being
|
||||
;; not "usable with production React", but no explanation why.
|
||||
;; If we decide to keep it, can be moved to `test-helpers.component`.
|
||||
(defn act
|
||||
"Run f to trigger Reagent updates,
|
||||
will return Promise which will resolve after
|
||||
Reagent and React render."
|
||||
[f]
|
||||
(js/Promise.
|
||||
(fn [resolve reject]
|
||||
(try
|
||||
(.then (rtl/act
|
||||
#(let [p (js/Promise. (fn [resolve _reject]
|
||||
(r/after-render (fn reagent-act-after-reagent-flush []
|
||||
(resolve)))))]
|
||||
(f)
|
||||
p))
|
||||
resolve
|
||||
reject)
|
||||
(catch :default e
|
||||
(reject e))))))
|
||||
|
||||
(def ^:private gesture-state
|
||||
{:untedermined 0
|
||||
:failed 1
|
||||
:began 2
|
||||
:cancelled 3
|
||||
:active 4
|
||||
:end 5})
|
||||
|
||||
(defn gesture-x-event
|
||||
[event position]
|
||||
(clj->js {:state (event gesture-state)
|
||||
:translationX position}))
|
||||
|
||||
(defn slide-events
|
||||
[dest]
|
||||
[(gesture-x-event :began 0)
|
||||
(gesture-x-event :active 0)
|
||||
(gesture-x-event :active dest)
|
||||
(gesture-x-event :end dest)])
|
||||
|
||||
(defn get-by-gesture-test-id
|
||||
[test-id]
|
||||
(gestures-jest/getByGestureTestId
|
||||
(str test-id)))
|
||||
|
||||
(def ^:private default-props
|
||||
{:on-complete identity
|
||||
:track-text :test-track-text
|
||||
:track-icon :face-id})
|
||||
|
||||
(h/describe "slide-button"
|
||||
(h/test "render the correct text"
|
||||
(h/render [slide-button/view default-props])
|
||||
(h/is-truthy (h/get-by-text :test-track-text)))
|
||||
|
||||
(h/test "render the disabled button"
|
||||
(h/render [slide-button/view (assoc default-props :disabled? true)])
|
||||
(let [track-mock (h/get-by-test-id :slide-button-track)]
|
||||
(h/has-style track-mock {:opacity constants/disable-opacity})))
|
||||
|
||||
(h/test "render the small button"
|
||||
(h/render [slide-button/view (assoc default-props :size :small)])
|
||||
(let [mock (h/get-by-test-id :slide-button-track)
|
||||
small-height (:track-height constants/small-dimensions)]
|
||||
(h/has-style mock {:height small-height})))
|
||||
|
||||
(h/test "render with the correct customization-color"
|
||||
(h/render [slide-button/view (assoc default-props :customization-color :purple)])
|
||||
(let [track-mock (h/get-by-test-id :slide-button-track)
|
||||
purple-color (utils/slider-color :track :purple)]
|
||||
(h/has-style track-mock {:backgroundColor purple-color})))
|
||||
|
||||
(h/test
|
||||
"calls on-complete when dragged"
|
||||
(let [props (merge default-props {:on-complete (h/mock-fn)})
|
||||
slide-dest constants/default-width
|
||||
gesture-events (slide-events slide-dest)]
|
||||
(h/render [slide-button/view props])
|
||||
(-> (act #(gestures-jest/fireGestureHandler (get-by-gesture-test-id :slide-button-gestures)
|
||||
gesture-events))
|
||||
(.then #(h/was-called (:on-complete props))))))
|
||||
|
||||
(h/test
|
||||
"doesn't call on-complete if the slide was incomplete"
|
||||
(let [props (merge default-props {:on-complete (h/mock-fn)})
|
||||
slide-dest (- constants/default-width 100)
|
||||
gesture-events (slide-events slide-dest)]
|
||||
(h/render [slide-button/view props])
|
||||
(-> (act #(gestures-jest/fireGestureHandler (get-by-gesture-test-id :slide-button-gestures)
|
||||
gesture-events))
|
||||
(.then #(h/was-not-called (:on-complete props)))))))
|
|
@ -0,0 +1,15 @@
|
|||
(ns quo2.components.buttons.slide-button.constants)
|
||||
|
||||
(def track-padding 4)
|
||||
|
||||
(def small-dimensions
|
||||
{:track-height 40
|
||||
:thumb 32})
|
||||
|
||||
(def large-dimensions
|
||||
{:track-height 48
|
||||
:thumb 40})
|
||||
|
||||
(def disable-opacity 0.3)
|
||||
|
||||
(def default-width 300)
|
|
@ -0,0 +1,80 @@
|
|||
(ns quo2.components.buttons.slide-button.style
|
||||
(:require
|
||||
[quo2.components.buttons.slide-button.constants :as constants]
|
||||
[quo2.components.buttons.slide-button.utils :as utils]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[quo2.foundations.typography :as typography]))
|
||||
|
||||
(def absolute-fill
|
||||
{:position :absolute
|
||||
:top 0
|
||||
:bottom 0
|
||||
:left 0
|
||||
:right 0})
|
||||
|
||||
(defn thumb-container
|
||||
[interpolate-track thumb-size customization-color]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:transform [{:translate-x (interpolate-track :track-clamp)}]}
|
||||
{:background-color (utils/slider-color :main customization-color)
|
||||
:border-radius 12
|
||||
:height thumb-size
|
||||
:width thumb-size
|
||||
:align-items :center
|
||||
:overflow :hidden
|
||||
:justify-content :center}))
|
||||
|
||||
(defn arrow-icon-container
|
||||
[interpolate-track]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:transform [{:translate-x (interpolate-track :arrow-icon-position)}]}
|
||||
{:flex 1
|
||||
:align-items :center
|
||||
:justify-content :center}))
|
||||
|
||||
(defn action-icon
|
||||
[interpolate-track size]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:transform [{:translate-x (interpolate-track :action-icon-position)}]}
|
||||
{:height size
|
||||
:width size
|
||||
:position :absolute
|
||||
:align-items :center
|
||||
:left 0
|
||||
:top 0
|
||||
:flex-direction :row
|
||||
:justify-content :space-around}))
|
||||
|
||||
(defn track
|
||||
[disabled? customization-color height]
|
||||
{:align-items :flex-start
|
||||
:justify-content :center
|
||||
:border-radius 14
|
||||
:height height
|
||||
:align-self :stretch
|
||||
:padding constants/track-padding
|
||||
:opacity (if disabled? 0.3 1)
|
||||
:background-color (utils/slider-color :track customization-color)})
|
||||
|
||||
(defn track-cover
|
||||
[interpolate-track]
|
||||
(reanimated/apply-animations-to-style
|
||||
{:left (interpolate-track :track-cover)}
|
||||
(assoc absolute-fill :overflow :hidden)))
|
||||
|
||||
(defn track-cover-text-container
|
||||
[track-width]
|
||||
{:position :absolute
|
||||
:right 0
|
||||
:top 0
|
||||
:bottom 0
|
||||
:align-items :center
|
||||
:justify-content :center
|
||||
:flex-direction :row
|
||||
:width track-width})
|
||||
|
||||
(defn track-text
|
||||
[customization-color]
|
||||
(-> typography/paragraph-1
|
||||
(merge typography/font-medium)
|
||||
(assoc :color (utils/slider-color :main customization-color))))
|
|
@ -0,0 +1,38 @@
|
|||
(ns quo2.components.buttons.slide-button.utils
|
||||
(:require
|
||||
[quo2.components.buttons.slide-button.constants :as constants]
|
||||
[quo2.foundations.colors :as colors]))
|
||||
|
||||
(defn slider-color
|
||||
"- `color-key` `:main`/`:track`
|
||||
- `customization-color` Customization color"
|
||||
[color-key customization-color]
|
||||
(let [colors-by-key {:main (colors/custom-color-by-theme customization-color 50 60)
|
||||
:track (colors/custom-color-by-theme customization-color 50 60 10 10)}]
|
||||
(color-key colors-by-key)))
|
||||
|
||||
(defn clamp-value
|
||||
[value min-value max-value]
|
||||
(cond
|
||||
(< value min-value) min-value
|
||||
(> value max-value) max-value
|
||||
:else value))
|
||||
|
||||
(defn calc-usable-track
|
||||
"Calculate the track section in which the
|
||||
thumb can move in. Mostly used for interpolations."
|
||||
[track-width thumb-size]
|
||||
(let [double-padding (* constants/track-padding 2)]
|
||||
(- track-width double-padding thumb-size)))
|
||||
|
||||
(defn get-dimensions
|
||||
[track-width size dimension-key]
|
||||
(let [default-dimensions (case size
|
||||
:small constants/small-dimensions
|
||||
:large constants/large-dimensions
|
||||
constants/large-dimensions)]
|
||||
(-> default-dimensions
|
||||
(merge {:usable-track (calc-usable-track
|
||||
track-width
|
||||
(:thumb default-dimensions))})
|
||||
(get dimension-key))))
|
|
@ -0,0 +1,89 @@
|
|||
(ns quo2.components.buttons.slide-button.view
|
||||
(:require
|
||||
[quo2.components.icon :as icon]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[quo2.components.buttons.slide-button.style :as style]
|
||||
[quo2.components.buttons.slide-button.utils :as utils]
|
||||
[quo2.components.buttons.slide-button.animations :as animations]
|
||||
[react-native.gesture :as gesture]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
[oops.core :as oops]
|
||||
[react-native.reanimated :as reanimated]
|
||||
[quo2.components.buttons.slide-button.constants :as constants]))
|
||||
|
||||
(defn- f-slider
|
||||
[{:keys [disabled?]}]
|
||||
(let [track-width (reagent/atom nil)
|
||||
sliding-complete? (reagent/atom false)
|
||||
gestures-disabled? (reagent/atom disabled?)
|
||||
on-track-layout (fn [evt]
|
||||
(let [width (oops/oget evt "nativeEvent.layout.width")]
|
||||
(reset! track-width width)))]
|
||||
|
||||
(fn [{:keys [on-complete
|
||||
track-text
|
||||
track-icon
|
||||
disabled?
|
||||
customization-color
|
||||
size]}]
|
||||
(let [x-pos (reanimated/use-shared-value 0)
|
||||
dimensions (partial utils/get-dimensions
|
||||
(or @track-width constants/default-width)
|
||||
size)
|
||||
interpolate-track (partial animations/interpolate-track
|
||||
x-pos
|
||||
(dimensions :usable-track)
|
||||
(dimensions :thumb))]
|
||||
|
||||
(rn/use-effect (fn []
|
||||
(when @sliding-complete?
|
||||
(on-complete)))
|
||||
[@sliding-complete?])
|
||||
|
||||
[gesture/gesture-detector
|
||||
{:gesture (animations/drag-gesture x-pos
|
||||
gestures-disabled?
|
||||
disabled?
|
||||
(dimensions :usable-track)
|
||||
sliding-complete?)}
|
||||
[reanimated/view
|
||||
{:test-ID :slide-button-track
|
||||
:style (style/track disabled? customization-color (dimensions :track-height))
|
||||
:on-layout (when-not (some? @track-width)
|
||||
on-track-layout)}
|
||||
[reanimated/view {:style (style/track-cover interpolate-track)}
|
||||
[rn/view {:style (style/track-cover-text-container @track-width)}
|
||||
[icon/icon track-icon
|
||||
{:color (utils/slider-color :main customization-color)
|
||||
:size 20}]
|
||||
[rn/view {:width 4}]
|
||||
[rn/text {:style (style/track-text customization-color)} track-text]]]
|
||||
[reanimated/view
|
||||
{:style (style/thumb-container interpolate-track
|
||||
(dimensions :thumb)
|
||||
customization-color)}
|
||||
[reanimated/view {:style (style/arrow-icon-container interpolate-track)}
|
||||
[icon/icon :arrow-right
|
||||
{:color colors/white
|
||||
:size 20}]]
|
||||
[reanimated/view
|
||||
{:style (style/action-icon interpolate-track
|
||||
(dimensions :thumb))}
|
||||
[icon/icon track-icon
|
||||
{:color colors/white
|
||||
:size 20}]]]]]))))
|
||||
|
||||
(defn view
|
||||
"Options
|
||||
- `on-complete` Callback called when the sliding is complete
|
||||
- `disabled?` Boolean that disables the button
|
||||
(_and gestures_)
|
||||
- `size` `:small`/`:large`
|
||||
- `track-text` Text that is shown on the track
|
||||
- `track-icon` Key of the icon shown on the track
|
||||
(e.g. `:face-id`)
|
||||
- `customization-color` Customization color
|
||||
"
|
||||
[props]
|
||||
[:f> f-slider props])
|
|
@ -11,6 +11,7 @@
|
|||
quo2.components.buttons.button
|
||||
quo2.components.buttons.dynamic-button
|
||||
quo2.components.buttons.predictive-keyboard.view
|
||||
quo2.components.buttons.slide-button.view
|
||||
quo2.components.colors.color-picker.view
|
||||
quo2.components.community.community-card-view
|
||||
quo2.components.community.community-list-view
|
||||
|
@ -130,6 +131,7 @@
|
|||
(def button quo2.components.buttons.button/button)
|
||||
(def dynamic-button quo2.components.buttons.dynamic-button/dynamic-button)
|
||||
(def predictive-keyboard quo2.components.buttons.predictive-keyboard.view/view)
|
||||
(def slide-button quo2.components.buttons.slide-button.view/view)
|
||||
|
||||
;;;; CARDS
|
||||
(def small-option-card quo2.components.onboarding.small-option-card.view/small-option-card)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
[quo2.components.banners.banner.component-spec]
|
||||
[quo2.components.buttons.--tests--.buttons-component-spec]
|
||||
[quo2.components.buttons.predictive-keyboard.component-spec]
|
||||
[quo2.components.buttons.slide-button.component-spec]
|
||||
[quo2.components.colors.color-picker.component-spec]
|
||||
[quo2.components.counter.--tests--.counter-component-spec]
|
||||
[quo2.components.counter.step.component-spec]
|
||||
|
|
|
@ -33,12 +33,16 @@
|
|||
|
||||
(defn max-pointers [gesture count] (.maxPointers ^js gesture count))
|
||||
|
||||
(defn min-distance [gesture dist] (.minDistance ^js gesture dist))
|
||||
|
||||
(defn number-of-taps [gesture count] (.numberOfTaps ^js gesture count))
|
||||
|
||||
(defn enabled [gesture enabled?] (.enabled ^js gesture enabled?))
|
||||
|
||||
(defn average-touches [gesture average-touches?] (.averageTouches ^js gesture average-touches?))
|
||||
|
||||
(defn with-test-ID [gesture test-ID] (.withTestId ^js gesture (str test-ID)))
|
||||
|
||||
(defn simultaneous
|
||||
([g1 g2] (.Simultaneous ^js Gesture g1 g2))
|
||||
([g1 g2 g3] (.Simultaneous ^js Gesture g1 g2 g3)))
|
||||
|
@ -77,7 +81,6 @@
|
|||
|
||||
(def scroll-view (reagent/adapt-react-class ScrollView))
|
||||
|
||||
|
||||
;;; Custom gesture section-list
|
||||
(defn- flatten-sections
|
||||
[sections]
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
(ns status-im2.contexts.quo-preview.buttons.slide-button
|
||||
(:require [quo2.core :as quo]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[react-native.core :as rn]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.contexts.quo-preview.preview :as preview]))
|
||||
|
||||
(def descriptor
|
||||
[{:label "Size:"
|
||||
:key :size
|
||||
:type :select
|
||||
:options [{:key :large
|
||||
:value "Large"}
|
||||
{:key :small
|
||||
:value "Small"}]}
|
||||
{:label "Disabled:"
|
||||
:key :disabled?
|
||||
:type :boolean}
|
||||
{:label "Custom Color"
|
||||
:key :color
|
||||
:type :select
|
||||
:options (map (fn [color]
|
||||
(let [key (get color :name)]
|
||||
{:key key :value key}))
|
||||
(quo/picker-colors))}])
|
||||
|
||||
(defn cool-preview
|
||||
[]
|
||||
(let [state (reagent/atom {:disabled? false
|
||||
:color :blue
|
||||
:size :large})
|
||||
color (reagent/cursor state [:color])
|
||||
disabled? (reagent/cursor state [:disabled?])
|
||||
size (reagent/cursor state [:size])
|
||||
complete? (reagent/atom false)]
|
||||
(fn []
|
||||
[rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
|
||||
[rn/view {:padding-bottom 150}
|
||||
[preview/customizer state descriptor]
|
||||
[rn/view
|
||||
{:padding-vertical 60
|
||||
:padding-horizontal 40
|
||||
:align-items :center}
|
||||
(if (not @complete?)
|
||||
[quo/slide-button
|
||||
{:track-text "We gotta slide"
|
||||
:track-icon :face-id
|
||||
:customization-color @color
|
||||
:size @size
|
||||
:disabled? @disabled?
|
||||
:on-complete (fn []
|
||||
(js/setTimeout (fn [] (reset! complete? true))
|
||||
1000)
|
||||
(js/alert "I don't wanna slide anymore"))}]
|
||||
[quo/button {:on-press (fn [] (reset! complete? false))}
|
||||
"Try again"])]]])))
|
||||
|
||||
(defn preview-slide-button
|
||||
[]
|
||||
[rn/view
|
||||
{:background-color (colors/theme-colors colors/white colors/neutral-90)
|
||||
:flex 1}
|
||||
[rn/flat-list
|
||||
{:flex 1
|
||||
:keyboard-should-persist-taps :always
|
||||
:header [cool-preview]
|
||||
:key-fn str}]])
|
|
@ -17,6 +17,7 @@
|
|||
[status-im2.contexts.quo-preview.avatars.wallet-user-avatar :as wallet-user-avatar]
|
||||
[status-im2.contexts.quo-preview.banners.banner :as banner]
|
||||
[status-im2.contexts.quo-preview.buttons.button :as button]
|
||||
[status-im2.contexts.quo-preview.buttons.slide-button :as slide-button]
|
||||
[status-im2.contexts.quo-preview.buttons.dynamic-button :as dynamic-button]
|
||||
[status-im2.contexts.quo-preview.buttons.predictive-keyboard :as predictive-keyboard]
|
||||
[status-im2.contexts.quo-preview.code.snippet :as code-snippet]
|
||||
|
@ -130,6 +131,9 @@
|
|||
{:name :dynamic-button
|
||||
:options {:topBar {:visible true}}
|
||||
:component dynamic-button/preview-dynamic-button}
|
||||
{:name :slide-button
|
||||
:options {:topBar {:visible true}}
|
||||
:component slide-button/preview-slide-button}
|
||||
{:name :predictive-keyboard
|
||||
:options {:topBar {:visible true}}
|
||||
:component predictive-keyboard/preview-predictive-keyboard}]
|
||||
|
|
|
@ -211,3 +211,7 @@
|
|||
(defn was-not-called
|
||||
[mock]
|
||||
(was-called-times mock 0))
|
||||
|
||||
(defn has-style
|
||||
[mock styles]
|
||||
(.toHaveStyle (js/expect mock) (clj->js styles)))
|
||||
|
|
Loading…
Reference in New Issue