Refactor user-avatar & add tests

This commit is contained in:
Ulises Manuel Cárdenas 2023-03-08 11:07:37 -06:00 committed by GitHub
parent e9310a2ace
commit 8478ac74ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 312 additions and 152 deletions

View File

@ -1,138 +0,0 @@
(ns quo2.components.avatars.user-avatar
(:require [clojure.string :as string]
[quo2.components.markdown.text :as text]
[quo2.foundations.colors :as colors]
[quo2.theme :refer [dark?]]
[react-native.core :as rn]
[react-native.fast-image :as fast-image]))
(def sizes
{:big {:outer 80
:inner 72
:status-indicator 20
:status-indicator-border 4
:font-size :heading-1}
:medium {:outer 48
:inner 44
:status-indicator 12
:status-indicator-border 2
:font-size :paragraph-1}
:small {:outer 32
:inner 28
:status-indicator 12
:status-indicator-border 2
:font-size :paragraph-2}
:xs {:outer 24
:inner 24
:status-indicator 0
:status-indicator-border 0
:font-size :paragraph-2}
:xxs {:outer 20
:inner 20
:status-indicator 0
:status-indicator-border 0
:font-size :label}
:xxxs {:outer 16
:inner 16
:status-indicator 0
:status-indicator-border 0
:font-size :label}})
(defn dot-indicator
[{:keys [size online? ring? dark?]}]
(let [dimensions (get-in sizes [size :status-indicator])
border-width (get-in sizes [size :status-indicator-border])
right (case size
:big 2
:medium 0
:small -2
0)
bottom (case size
:big (if ring? -1 2)
:medium (if ring? 0 -2)
:small -2
0)]
[rn/view
{:style {:background-color (if online?
colors/success-50
colors/neutral-40)
:width dimensions
:height dimensions
:border-width border-width
:border-radius dimensions
:border-color (if dark?
colors/neutral-100
colors/white)
:position :absolute
:bottom bottom
:right right}}]))
(defn initials-style
[inner-dimensions outer-dimensions]
{:position :absolute
:top (/ (- outer-dimensions inner-dimensions) 2)
:left (/ (- outer-dimensions inner-dimensions) 2)
:width inner-dimensions
:height inner-dimensions
:border-radius inner-dimensions
:justify-content :center
:align-items :center
:background-color (colors/custom-color-by-theme :turquoise 50 60)})
(defn outer-styles
[outer-dimensions]
{:width outer-dimensions
:height outer-dimensions
:border-radius outer-dimensions})
(def one-initial-letter-sizes #{:xs :xxs :xxxs})
(def valid-ring-sizes #{:big :medium :small})
(defn initials-avatar
[{:keys [full-name size inner-dimensions outer-dimensions]}]
(let [amount-initials (if (one-initial-letter-sizes size) 1 2)
initials (as-> full-name $
(string/split $ " ")
(map (comp string/upper-case first) $)
(take amount-initials $)
(string/join $))
font-size (get-in sizes [size :font-size])]
[rn/view {:style (initials-style inner-dimensions outer-dimensions)}
[text/text
{:style {:color colors/white-opa-70}
:weight :semi-bold
:size font-size}
initials]]))
(defn user-avatar
"If no `profile-picture` is given, draws the initials based on the `full-name` and
uses `ring-background` to display the ring behind the initials when given. Otherwise,
shows the profile picture which already comes with the ring drawn over it."
[{:keys [full-name status-indicator? online? size profile-picture ring-background]
:or {status-indicator? true
online? true
size :big}}]
(let [full-name (or full-name "empty name")
draw-ring? (and ring-background (valid-ring-sizes size))
outer-dimensions (get-in sizes [size :outer])
inner-dimensions (get-in sizes [size (if draw-ring? :inner :outer)])]
[rn/view
{:style (outer-styles outer-dimensions)
:accessibility-label :user-avatar}
;; The `profile-picture` already has the ring in it
(when-let [image (or profile-picture ring-background)]
[fast-image/fast-image
{:style (outer-styles outer-dimensions)
:source image}])
(when-not profile-picture
[initials-avatar
{:full-name full-name
:size size
:inner-dimensions inner-dimensions
:outer-dimensions outer-dimensions}])
(when status-indicator?
[dot-indicator
{:size size
:online? online?
:ring? draw-ring?
:dark? (dark?)}])]))

View File

@ -0,0 +1,129 @@
(ns quo2.components.avatars.user-avatar.component-spec
(:require [quo2.components.avatars.user-avatar.view :as user-avatar]
[test-helpers.component :as h]))
(defonce mock-picture (js/require "../resources/images/mock2/user_picture_male4.png"))
(h/describe "user avatar"
(h/test "Default render"
(h/render [user-avatar/user-avatar])
(h/is-truthy (h/get-by-label-text :user-avatar))
(h/is-truthy (h/get-by-text "EN")))
(h/describe "Profile picture"
(h/test "Renders"
(h/render
[user-avatar/user-avatar {:profile-picture mock-picture}])
(h/is-truthy (h/get-by-label-text :profile-picture)))
(h/test "Renders even if `:full-name` is passed"
(h/render
[user-avatar/user-avatar
{:profile-picture mock-picture
:full-name "New User1"}])
(h/is-truthy (h/get-by-label-text :profile-picture))
(h/is-null (h/query-by-label-text :initials-avatar)))
(h/describe "Status indicator"
(h/test "Render"
(h/render
[user-avatar/user-avatar
{:profile-picture mock-picture
:status-indicator? true}])
(h/is-truthy (h/get-by-label-text :profile-picture))
(h/is-truthy (h/get-by-label-text :status-indicator)))
(h/test "Do not render"
(h/render
[user-avatar/user-avatar
{:profile-picture mock-picture
:status-indicator? false}])
(h/is-truthy (h/get-by-label-text :profile-picture))
(h/is-null (h/query-by-label-text :status-indicator)))))
(h/describe "Initials Avatar"
(h/describe "Render initials"
(letfn [(user-avatar-component [size]
[user-avatar/user-avatar
{:full-name "New User"
:size size}])]
(h/describe "Two letters"
(h/test "Size :big"
(h/render (user-avatar-component :big))
(h/is-truthy (h/get-by-text "NU")))
(h/test "Size :medium"
(h/render (user-avatar-component :medium))
(h/is-truthy (h/get-by-text "NU")))
(h/test "Size :small"
(h/render (user-avatar-component :small))
(h/is-truthy (h/get-by-text "NU"))))
(h/describe "One letter"
(h/test "Size :xs"
(h/render (user-avatar-component :xs))
(h/is-truthy (h/get-by-text "N")))
(h/test "Size :xxs"
(h/render (user-avatar-component :xxs))
(h/is-truthy (h/get-by-text "N")))
(h/test "Size :xxxs"
(h/render (user-avatar-component :xxxs))
(h/is-truthy (h/get-by-text "N"))))))
(h/describe "Render ring"
(letfn [(user-avatar-component [size]
[user-avatar/user-avatar
{:full-name "New User"
:ring-background mock-picture
:size size}])]
(h/describe "Passed and drawn"
(h/test "Size :big"
(h/render (user-avatar-component :big))
(h/is-truthy (h/get-by-label-text :initials-avatar))
(h/is-truthy (h/get-by-label-text :ring-background)))
(h/test "Size :medium"
(h/render (user-avatar-component :medium))
(h/is-truthy (h/get-by-label-text :initials-avatar))
(h/is-truthy (h/get-by-label-text :ring-background)))
(h/test "Size :small"
(h/render (user-avatar-component :small))
(h/is-truthy (h/get-by-label-text :initials-avatar))
(h/is-truthy (h/get-by-label-text :ring-background))))
(h/describe "Passed and not drawn (because of invalid size for ring)"
(h/test "Size :xs"
(h/render (user-avatar-component :xs))
(h/is-truthy (h/get-by-label-text :initials-avatar))
(h/is-null (h/query-by-label-text :ring-background)))
(h/test "Size :xxs"
(h/render (user-avatar-component :xxs))
(h/is-truthy (h/get-by-label-text :initials-avatar))
(h/is-null (h/query-by-label-text :ring-background)))
(h/test "Size :xxxs"
(h/render (user-avatar-component :xxxs))
(h/is-truthy (h/get-by-label-text :initials-avatar))
(h/is-null (h/query-by-label-text :ring-background))))))
(h/describe "Status indicator"
(h/test "Render"
(h/render
[user-avatar/user-avatar
{:full-name "Test User"
:status-indicator? true}])
(h/is-truthy (h/get-by-label-text :initials-avatar))
(h/is-truthy (h/get-by-label-text :status-indicator)))
(h/test "Do not render"
(h/render
[user-avatar/user-avatar
{:full-name "Test User"
:status-indicator? false}])
(h/is-truthy (h/get-by-label-text :initials-avatar))
(h/is-null (h/query-by-label-text :status-indicator))))))

View File

@ -0,0 +1,83 @@
(ns quo2.components.avatars.user-avatar.style
(:require [quo2.foundations.colors :as colors]))
(def sizes
{:big {:outer 80
:inner 72
:status-indicator 20
:status-indicator-border 4
:font-size :heading-1}
:medium {:outer 48
:inner 44
:status-indicator 12
:status-indicator-border 2
:font-size :paragraph-1}
:small {:outer 32
:inner 28
:status-indicator 12
:status-indicator-border 2
:font-size :paragraph-2}
:xs {:outer 24
:inner 24
:status-indicator 0
:status-indicator-border 0
:font-size :paragraph-2}
:xxs {:outer 20
:inner 20
:status-indicator 0
:status-indicator-border 0
:font-size :label}
:xxxs {:outer 16
:inner 16
:status-indicator 0
:status-indicator-border 0
:font-size :label}})
(defn outer
[size]
(let [dimensions (get-in sizes [size :outer])]
{:width dimensions
:height dimensions
:border-radius dimensions}))
(defn initials-avatar
[size draw-ring? customization-color]
(let [outer-dimensions (get-in sizes [size :outer])
inner-dimensions (get-in sizes [size (if draw-ring? :inner :outer)])]
{:position :absolute
:top (/ (- outer-dimensions inner-dimensions) 2)
:left (/ (- outer-dimensions inner-dimensions) 2)
:width inner-dimensions
:height inner-dimensions
:border-radius inner-dimensions
:justify-content :center
:align-items :center
:background-color (colors/custom-color-by-theme customization-color 50 60)}))
(def initials-avatar-text
{:color colors/white-opa-70})
(defn dot
[size online? ring?]
(let [background (if online? colors/success-50 colors/neutral-40)
dimensions (get-in sizes [size :status-indicator])
border-width (get-in sizes [size :status-indicator-border])
right (case size
:big 2
:medium 0
:small -2
0)
bottom (case size
:big (if ring? -1 2)
:medium (if ring? 0 -2)
:small -2
0)]
{:position :absolute
:bottom bottom
:right right
:width dimensions
:height dimensions
:border-width border-width
:border-radius dimensions
:border-color (colors/theme-colors colors/white colors/neutral-100)
:background-color background}))

View File

@ -0,0 +1,61 @@
(ns quo2.components.avatars.user-avatar.view
(:require [clojure.string :as string]
[quo2.components.avatars.user-avatar.style :as style]
[quo2.components.markdown.text :as text]
[react-native.core :as rn]
[react-native.fast-image :as fast-image]))
(defn- extract-initials
[full-name amount-initials]
(let [upper-case-first-letter (comp string/upper-case first)
names-list (string/split full-name " ")]
(->> names-list
(map upper-case-first-letter)
(take amount-initials)
(string/join))))
(defn initials-avatar
[{:keys [full-name size draw-ring? customization-color]}]
(let [font-size (get-in style/sizes [size :font-size])
amount-initials (if (#{:xs :xxs :xxxs} size) 1 2)]
[rn/view
{:accessibility-label :initials-avatar
:style (style/initials-avatar size draw-ring? customization-color)}
[text/text
{:style style/initials-avatar-text
:size font-size
:weight :semi-bold}
(extract-initials full-name amount-initials)]]))
(def valid-ring-sizes #{:big :medium :small})
(defn user-avatar
"If no `profile-picture` is given, draws the initials based on the `full-name` and
uses `ring-background` to display the ring behind the initials when given. Otherwise,
shows the `profile-picture` which already comes with the ring drawn."
[{:keys [full-name status-indicator? online? size profile-picture ring-background
customization-color]
:or {status-indicator? true
online? true
size :big
customization-color :turquoise}}]
(let [full-name (or full-name "empty name")
draw-ring? (and ring-background (valid-ring-sizes size))
outer-styles (style/outer size)]
[rn/view {:style outer-styles :accessibility-label :user-avatar}
;; The `profile-picture` already has the ring in it
(when-let [image (or profile-picture ring-background)]
[fast-image/fast-image
{:accessibility-label (if draw-ring? :ring-background :profile-picture)
:style outer-styles
:source image}])
(when-not profile-picture
[initials-avatar
{:full-name full-name
:size size
:draw-ring? draw-ring?
:customization-color customization-color}])
(when status-indicator?
[rn/view
{:accessibility-label :status-indicator
:style (style/dot size online? draw-ring?)}])]))

View File

@ -1,5 +1,5 @@
(ns quo2.components.list-items.preview-list (ns quo2.components.list-items.preview-list
(:require [quo2.components.avatars.user-avatar :as user-avatar] (:require [quo2.components.avatars.user-avatar.view :as user-avatar]
[quo2.components.icon :as quo2.icons] [quo2.components.icon :as quo2.icons]
[quo2.components.markdown.text :as quo2.text] [quo2.components.markdown.text :as quo2.text]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]

View File

@ -1,6 +1,6 @@
(ns quo2.components.list-items.user-list (ns quo2.components.list-items.user-list
(:require [react-native.core :as rn] (:require [react-native.core :as rn]
[quo2.components.avatars.user-avatar :as user-avatar] [quo2.components.avatars.user-avatar.view :as user-avatar]
[quo2.components.markdown.text :as text] [quo2.components.markdown.text :as text]
[quo2.components.icon :as icons] [quo2.components.icon :as icons]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]

View File

@ -1,6 +1,6 @@
(ns quo2.components.messages.system-message (ns quo2.components.messages.system-message
(:require [quo2.components.avatars.icon-avatar :as icon-avatar] (:require [quo2.components.avatars.icon-avatar :as icon-avatar]
[quo2.components.avatars.user-avatar :as user-avatar] [quo2.components.avatars.user-avatar.view :as user-avatar]
[quo2.components.markdown.text :as text] [quo2.components.markdown.text :as text]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[quo2.theme :as theme] [quo2.theme :as theme]

View File

@ -1,6 +1,6 @@
(ns quo2.components.navigation.page-nav (ns quo2.components.navigation.page-nav
(:require [clojure.string :as string] (:require [clojure.string :as string]
[quo2.components.avatars.user-avatar :as user-avatar] [quo2.components.avatars.user-avatar.view :as user-avatar]
[quo2.components.buttons.button :as button] [quo2.components.buttons.button :as button]
[quo2.components.icon :as icons] [quo2.components.icon :as icons]
[quo2.components.markdown.text :as text] [quo2.components.markdown.text :as text]

View File

@ -6,7 +6,7 @@
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[quo2.components.markdown.text :as text] [quo2.components.markdown.text :as text]
[quo2.components.buttons.button :as button] [quo2.components.buttons.button :as button]
[quo2.components.avatars.user-avatar :as user-avatar] [quo2.components.avatars.user-avatar.view :as user-avatar]
[quo2.components.profile.profile-card.style :as style])) [quo2.components.profile.profile-card.style :as style]))
(defn profile-card (defn profile-card

View File

@ -3,7 +3,7 @@
[quo2.components.profile.select-profile.style :as style] [quo2.components.profile.select-profile.style :as style]
[react-native.core :as rn] [react-native.core :as rn]
[quo2.components.markdown.text :as text] [quo2.components.markdown.text :as text]
[quo2.components.avatars.user-avatar :as user-avatar] [quo2.components.avatars.user-avatar.view :as user-avatar]
[reagent.core :as reagent])) [reagent.core :as reagent]))
(defn- on-change-handler (defn- on-change-handler

View File

@ -5,7 +5,7 @@
quo2.components.avatars.channel-avatar quo2.components.avatars.channel-avatar
quo2.components.avatars.group-avatar quo2.components.avatars.group-avatar
quo2.components.avatars.icon-avatar quo2.components.avatars.icon-avatar
quo2.components.avatars.user-avatar quo2.components.avatars.user-avatar.view
quo2.components.avatars.wallet-user-avatar quo2.components.avatars.wallet-user-avatar
quo2.components.banners.banner.view quo2.components.banners.banner.view
quo2.components.buttons.button quo2.components.buttons.button
@ -102,7 +102,7 @@
(def channel-avatar quo2.components.avatars.channel-avatar/channel-avatar) (def channel-avatar quo2.components.avatars.channel-avatar/channel-avatar)
(def group-avatar quo2.components.avatars.group-avatar/group-avatar) (def group-avatar quo2.components.avatars.group-avatar/group-avatar)
(def icon-avatar quo2.components.avatars.icon-avatar/icon-avatar) (def icon-avatar quo2.components.avatars.icon-avatar/icon-avatar)
(def user-avatar quo2.components.avatars.user-avatar/user-avatar) (def user-avatar quo2.components.avatars.user-avatar.view/user-avatar)
(def wallet-user-avatar quo2.components.avatars.wallet-user-avatar/wallet-user-avatar) (def wallet-user-avatar quo2.components.avatars.wallet-user-avatar/wallet-user-avatar)
;;;; BANNER ;;;; BANNER

View File

@ -1,5 +1,6 @@
(ns quo2.core-spec (ns quo2.core-spec
(:require [quo2.components.banners.banner.component-spec] (:require [quo2.components.avatars.user-avatar.component-spec]
[quo2.components.banners.banner.component-spec]
[quo2.components.buttons.--tests--.buttons-component-spec] [quo2.components.buttons.--tests--.buttons-component-spec]
[quo2.components.counter.--tests--.counter-component-spec] [quo2.components.counter.--tests--.counter-component-spec]
[quo2.components.dividers.--tests--.divider-label-component-spec] [quo2.components.dividers.--tests--.divider-label-component-spec]

View File

@ -1,5 +1,5 @@
(ns status-im2.contexts.quo-preview.avatars.user-avatar (ns status-im2.contexts.quo-preview.avatars.user-avatar
(:require [quo2.components.avatars.user-avatar :as quo2] (:require [quo2.components.avatars.user-avatar.view :as quo2]
[quo2.foundations.colors :as colors] [quo2.foundations.colors :as colors]
[react-native.core :as rn] [react-native.core :as rn]
[reagent.core :as reagent] [reagent.core :as reagent]
@ -22,6 +22,13 @@
:value "xx Small"} :value "xx Small"}
{:key :xxxs {:key :xxxs
:value "xxx Small"}]} :value "xxx Small"}]}
{:label "Customization color:"
:key :customization-color
:type :select
:options (map (fn [[color-kw _]]
{:key color-kw
:value (name color-kw)})
colors/customization)}
{:label "Online status" {:label "Online status"
:key :online? :key :online?
:type :boolean} :type :boolean}
@ -46,10 +53,11 @@
(defn cool-preview (defn cool-preview
[] []
(let [state (reagent/atom {:full-name "A Y" (let [state (reagent/atom {:full-name "A Y"
:status-indicator? true :status-indicator? true
:online? true :online? true
:size :medium})] :size :medium
:customization-color :blue})]
(fn [] (fn []
[rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!} [rn/touchable-without-feedback {:on-press rn/dismiss-keyboard!}
[rn/view {:padding-bottom 150} [rn/view {:padding-bottom 150}

View File

@ -30,10 +30,18 @@
[text] [text]
(rtl/screen.getByText text)) (rtl/screen.getByText text))
(defn find-by-text
[text]
(rtl/screen.findByText text))
(defn get-by-label-text (defn get-by-label-text
[label] [label]
(rtl/screen.getByLabelText (name label))) (rtl/screen.getByLabelText (name label)))
(defn query-by-label-text
[label]
(rtl/screen.queryByLabelText (name label)))
(defn get-by-translation-text (defn get-by-translation-text
[keyword] [keyword]
(get-by-text (str "tx:" (name keyword)))) (get-by-text (str "tx:" (name keyword))))
@ -55,3 +63,11 @@
(js/jest.advanceTimersByTime time-ms)) (js/jest.advanceTimersByTime time-ms))
(def mock-fn js/jest.fn) (def mock-fn js/jest.fn)
(defn is-truthy
[element]
(.toBeTruthy (js/expect element)))
(defn is-null
[element]
(.toBeNull (js/expect element)))