Introduce subscription tests (#14472)
This commit is contained in:
parent
da0f0d3a81
commit
6e272a96c8
|
@ -7,8 +7,11 @@
|
|||
quo.previews.preview/list-comp clojure.core/for
|
||||
status-im.utils.styles/def clojure.core/def
|
||||
status-im.utils.styles/defn clojure.core/defn
|
||||
status-im.test-helpers/deftest-sub clojure.core/defn
|
||||
taoensso.tufte/defnp clojure.core/defn}
|
||||
:linters {:invalid-arity {:skip-args [status-im.utils.fx/defn utils.re-frame/defn]}
|
||||
;;TODO remove number when this is fixed
|
||||
;;https://github.com/borkdude/clj-kondo/issues/867
|
||||
:unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY number]}}}
|
||||
;; TODO remove number when this is fixed
|
||||
;; https://github.com/borkdude/clj-kondo/issues/867
|
||||
:unresolved-symbol {:exclude [PersistentPriorityMap.EMPTY
|
||||
number
|
||||
status-im.test-helpers/restore-app-db]}}}
|
||||
|
|
|
@ -346,6 +346,53 @@ keywords and concatenating them into a single string.
|
|||
(i18n/label :t/biometric-auth-error {:code error-code})
|
||||
```
|
||||
|
||||
### Tests
|
||||
#### Subscription tests
|
||||
|
||||
Test [layer-3 subscriptions](https://day8.github.io/re-frame/subscriptions/) by
|
||||
actually subscribing to them, so reframe's signal graph gets validated too.
|
||||
|
||||
```clojure
|
||||
;; bad
|
||||
(defn user-recipes
|
||||
[[current-user all-recipes location]]
|
||||
...)
|
||||
|
||||
(re-frame/reg-sub
|
||||
:user/recipes
|
||||
:<- [:current-user]
|
||||
:<- [:all-recipes]
|
||||
:<- [:location]
|
||||
user-recipes)
|
||||
|
||||
(deftest user-recipes-test
|
||||
(testing "builds list of recipes"
|
||||
(let [current-user {...}
|
||||
all-recipes {...}
|
||||
location [...]]
|
||||
(is (= expected (recipes [current-user all-recipes location]))))))
|
||||
|
||||
;; good
|
||||
(require '[status-im.test-helpers :as h])
|
||||
|
||||
(re-frame/reg-sub
|
||||
:user/recipes
|
||||
:<- [:current-user]
|
||||
:<- [:all-recipes]
|
||||
:<- [:location]
|
||||
(fn [[current-user all-recipes location]]
|
||||
...))
|
||||
|
||||
(h/deftest-sub :user/recipes
|
||||
[sub-name]
|
||||
(testing "builds list of recipes"
|
||||
(swap! rf-db/app-db assoc
|
||||
:current-user {...}
|
||||
:all-recipes {...}
|
||||
:location [...])
|
||||
(is (= expected (rf/sub [sub-name])))))
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
First, the bird's-eye view with some example ClojureScript files:
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
(ns status-im.test-helpers
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[clojure.string :as string]
|
||||
[clojure.walk :as walk]))
|
||||
|
||||
(defn- subscription-name->test-name
|
||||
[sub-name]
|
||||
(->> [(namespace sub-name)
|
||||
(name sub-name)
|
||||
"test"]
|
||||
(remove nil?)
|
||||
(map #(string/replace % #"\." "-"))
|
||||
(string/join "-")))
|
||||
|
||||
(defmacro ^:private testing-subscription
|
||||
[description & body]
|
||||
`(cljs.test/testing ~description
|
||||
(restore-app-db (fn [] ~@body))))
|
||||
|
||||
(s/fdef deftest-sub
|
||||
:args (s/cat :sub-name keyword?
|
||||
:args (s/coll-of symbol? :count 1)
|
||||
:body (s/* any?)))
|
||||
|
||||
(defmacro deftest-sub
|
||||
"Defines a test based on `sub-name`, executes `body` and restores the app db.
|
||||
|
||||
Any usage of the `cljs.test/testing` macro inside `body` will be modified to
|
||||
also make sure the app db is restored and the subscription cache is reset.
|
||||
|
||||
Expressions in `body` will have access to `sub-name`, which should be used to
|
||||
avoid needlessly repeating the subscription name.
|
||||
|
||||
Example:
|
||||
|
||||
```clojure
|
||||
(require '[status-im.test-helpers :as h])
|
||||
|
||||
(h/deftest-sub :wallet/sorted-tokens
|
||||
[sub-name]
|
||||
(testing \"sorts tokens by name, lowercased\"
|
||||
;; Arrange
|
||||
(swap! rf-db/app-db assoc-in [<db-path>] <value>)
|
||||
|
||||
;; Act and Assert
|
||||
(is (= <expected> (rf/sub [sub-name])))))
|
||||
```"
|
||||
[sub-name args & body]
|
||||
`(let [sub-name# ~sub-name]
|
||||
(cljs.test/deftest ~(symbol (subscription-name->test-name sub-name))
|
||||
(let [~args [sub-name#]]
|
||||
(restore-app-db
|
||||
(fn []
|
||||
~@(clojure.walk/postwalk-replace
|
||||
{'cljs.test/testing `testing-subscription
|
||||
'testing `testing-subscription}
|
||||
body)))))))
|
|
@ -5,10 +5,12 @@
|
|||
Avoid coupling this namespace with particularities of the Status' domain, thus
|
||||
prefer to use it for more general purpose concepts, such as the re-frame event
|
||||
layer."
|
||||
(:require-macros status-im.test-helpers)
|
||||
(:require [re-frame.core :as rf]
|
||||
[re-frame.db :as rf-db]
|
||||
[re-frame.events :as rf-events]
|
||||
[re-frame.registrar :as rf-registrar]
|
||||
[re-frame.subs :as rf-subs]
|
||||
[taoensso.timbre :as log]))
|
||||
|
||||
(defn db
|
||||
|
@ -100,3 +102,15 @@
|
|||
:fn (fn [{:keys [vargs level]}]
|
||||
(swap! logs conj {:args vargs :level level}))})]
|
||||
(f logs))))
|
||||
|
||||
(defn restore-app-db
|
||||
"Saves current app DB, calls `f` and restores the original app DB.
|
||||
|
||||
Always clears the subscription cache after calling `f`."
|
||||
[f]
|
||||
(rf-subs/clear-subscription-cache!)
|
||||
(let [original-db @rf-db/app-db]
|
||||
(try
|
||||
(f)
|
||||
(finally
|
||||
(reset! rf-db/app-db original-db)))))
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
(ns status-im2.subs.activity-center-test
|
||||
(:require [cljs.test :refer [is testing]]
|
||||
[re-frame.db :as rf-db]
|
||||
[status-im.test-helpers :as h]
|
||||
status-im2.subs.activity-center
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(h/deftest-sub :activity-center/filter-status-unread-enabled?
|
||||
[sub-name]
|
||||
(testing "returns true when filter status is unread"
|
||||
(swap! rf-db/app-db assoc-in [:activity-center :filter :status] :unread)
|
||||
(is (true? (rf/sub [sub-name]))))
|
||||
|
||||
(testing "returns false when filter status is not unread"
|
||||
(swap! rf-db/app-db assoc-in [:activity-center :filter :status] :all)
|
||||
(is (false? (rf/sub [sub-name])))))
|
|
@ -1,40 +1,76 @@
|
|||
(ns status-im2.subs.communities-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[status-im2.subs.communities :as subs]))
|
||||
(:require [cljs.test :refer [is testing use-fixtures]]
|
||||
[re-frame.db :as rf-db]
|
||||
[status-im.test-helpers :as h]
|
||||
status-im2.subs.communities
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(deftest community->home-item-test
|
||||
(testing "has unread messages"
|
||||
(is (= {:name "name-1"
|
||||
:muted? true
|
||||
:unread-messages? true
|
||||
:unread-mentions-count 5
|
||||
:community-icon "icon-1"}
|
||||
(subs/community->home-item
|
||||
{:name "name-1"
|
||||
:muted true
|
||||
:images {:thumbnail {:uri "icon-1"}}}
|
||||
{:unviewed-messages-count 1
|
||||
:unviewed-mentions-count 5}))))
|
||||
(testing "no unread messages"
|
||||
(is (= {:name "name-2"
|
||||
:muted? false
|
||||
:unread-messages? false
|
||||
:unread-mentions-count 5
|
||||
:community-icon "icon-2"}
|
||||
(subs/community->home-item
|
||||
{:name "name-2"
|
||||
:muted false
|
||||
:images {:thumbnail {:uri "icon-2"}}}
|
||||
{:unviewed-messages-count 0
|
||||
:unviewed-mentions-count 5})))))
|
||||
(use-fixtures :each
|
||||
{:before #(reset! rf-db/app-db {:communities/enabled? true})})
|
||||
|
||||
(deftest calculate-unviewed-counts-test
|
||||
(let [chats [{:unviewed-messages-count 1
|
||||
:unviewed-mentions-count 2}
|
||||
{:unviewed-messages-count 3
|
||||
(def community-id "0x1")
|
||||
|
||||
(h/deftest-sub :communities
|
||||
[sub-name]
|
||||
(testing "returns empty vector if flag is disabled"
|
||||
(swap! rf-db/app-db assoc :communities/enabled? false)
|
||||
(is (= [] (rf/sub [sub-name]))))
|
||||
|
||||
(testing "returns raw communities if flag is enabled"
|
||||
(let [raw-communities {"0x1" {:id "0x1"}}]
|
||||
(swap! rf-db/app-db assoc
|
||||
:communities/enabled? true
|
||||
:communities raw-communities)
|
||||
(is (= raw-communities (rf/sub [sub-name]))))))
|
||||
|
||||
(h/deftest-sub :communities/section-list
|
||||
[sub-name]
|
||||
(testing "builds sections using the first community name char (uppercased)"
|
||||
(swap! rf-db/app-db assoc :communities
|
||||
{"0x1" {:name "civilized monkeys"}
|
||||
"0x2" {:name "Civilized rats"}})
|
||||
(is (= [{:title "C"
|
||||
:data [{:name "civilized monkeys"}
|
||||
{:name "Civilized rats"}]}]
|
||||
(rf/sub [sub-name]))))
|
||||
|
||||
(testing "sorts by section ascending"
|
||||
(swap! rf-db/app-db assoc :communities
|
||||
{"0x3" {:name "Memorable"}
|
||||
"0x1" {:name "Civilized monkeys"}})
|
||||
(is (= [{:title "C" :data [{:name "Civilized monkeys"}]}
|
||||
{:title "M" :data [{:name "Memorable"}]}]
|
||||
(rf/sub [sub-name]))))
|
||||
|
||||
(testing "builds default section for communities without a name"
|
||||
(swap! rf-db/app-db assoc :communities
|
||||
{"0x2" {:id "0x2"}
|
||||
"0x1" {:id "0x1"}})
|
||||
(is (= [{:title ""
|
||||
:data [{:id "0x2"}
|
||||
{:id "0x1"}]}]
|
||||
(rf/sub [sub-name])))))
|
||||
|
||||
(h/deftest-sub :communities/unviewed-counts
|
||||
[sub-name]
|
||||
(testing "sums counts for a particular community"
|
||||
(swap! rf-db/app-db assoc :chats
|
||||
{"0x100" {:community-id community-id
|
||||
:unviewed-mentions-count 3
|
||||
:unviewed-messages-count 2}
|
||||
"0x101" {:community-id "0x2"
|
||||
:unviewed-mentions-count 7
|
||||
:unviewed-messages-count 9}
|
||||
"0x102" {:community-id community-id
|
||||
:unviewed-mentions-count 5
|
||||
:unviewed-messages-count 1}})
|
||||
(is (= {:unviewed-messages-count 3
|
||||
:unviewed-mentions-count 8}
|
||||
(rf/sub [sub-name community-id]))))
|
||||
|
||||
(testing "defaults to zero when count keys are not present"
|
||||
(swap! rf-db/app-db assoc :chats
|
||||
{"0x100" {:community-id community-id}})
|
||||
(is (= {:unviewed-messages-count 0
|
||||
:unviewed-mentions-count 0}
|
||||
{:unviewed-messages-count 2
|
||||
:unviewed-mentions-count 1}]]
|
||||
(is (= {:unviewed-messages-count 6
|
||||
:unviewed-mentions-count 3}
|
||||
(subs/calculate-unviewed-counts chats)))))
|
||||
(rf/sub [sub-name community-id])))))
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
(ns status-im2.subs.subs-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[status-im2.subs.wallet.transactions :as wallet.transactions]
|
||||
[status-im2.subs.onboarding :as onboarding]
|
||||
[status-im.utils.money :as money]
|
||||
[status-im2.subs.wallet.wallet :as wallet]))
|
||||
[status-im2.subs.onboarding :as onboarding]))
|
||||
|
||||
(def transactions [{:timestamp "1505912551000"}
|
||||
{:timestamp "1505764322000"}
|
||||
|
@ -40,17 +38,3 @@
|
|||
{"0x1" {:keycard-pairing "keycard-pairing-code"}}}
|
||||
{})]
|
||||
(is (= res "keycard-pairing-code")))))
|
||||
|
||||
(deftest test-balance-total-value
|
||||
(is (= (wallet/get-balance-total-value
|
||||
{:ETH (money/bignumber 1000000000000000000)
|
||||
:SNT (money/bignumber 100000000000000000000)
|
||||
:AST (money/bignumber 10000)}
|
||||
{:ETH {:USD {:from "ETH", :to "USD", :price 677.91, :last-day 658.688}}
|
||||
:SNT {:USD {:from "SNT", :to "USD", :price 0.1562, :last-day 0.15}}
|
||||
:AST {:USD {:from "AST", :to "USD", :price 4, :last-day 3}}}
|
||||
:USD
|
||||
{:ETH 18
|
||||
:SNT 18
|
||||
:AST 4})
|
||||
697.53)))
|
|
@ -0,0 +1,133 @@
|
|||
(ns status-im2.subs.wallet.wallet-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[re-frame.db :as rf-db]
|
||||
[status-im.test-helpers :as h]
|
||||
[status-im.utils.money :as money]
|
||||
[status-im2.subs.wallet.wallet :as wallet]
|
||||
[utils.re-frame :as rf]))
|
||||
|
||||
(def money-zero (money/bignumber 0))
|
||||
(def money-eth (money/bignumber 8000000000000000000))
|
||||
(def money-snt (money/bignumber 756000000000000000000))
|
||||
(def main-account-id "0x0Fbd")
|
||||
|
||||
(def accounts
|
||||
[{:address "0x0Fbd"
|
||||
:name "Main account"
|
||||
:hidden false
|
||||
:removed false}
|
||||
{:address "0x5B03"
|
||||
:name "Secondary account"
|
||||
:hidden false
|
||||
:removed false}])
|
||||
|
||||
(def wallet
|
||||
{:accounts {main-account-id
|
||||
{:balance {:ETH money-eth :SNT money-snt}
|
||||
:transactions {}
|
||||
:max-block 0}
|
||||
"0x5B03"
|
||||
{:balance {:ETH money-eth :SNT money-snt}
|
||||
:transactions {}
|
||||
:max-block 10}}})
|
||||
|
||||
(def prices
|
||||
{:ETH {:USD {:from "ETH"
|
||||
:to "USD"
|
||||
:price 1282.23}}
|
||||
:SNT {:USD {:from "SNT"
|
||||
:to "USD"
|
||||
:price 0.0232}}})
|
||||
|
||||
(def tokens
|
||||
{"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||
{:address "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||
:name "Ether"
|
||||
:symbol :ETH
|
||||
:decimals 18
|
||||
:chainId 1}
|
||||
"0x744d70fdbe2ba4cf95131626614a1763df805b9e"
|
||||
{:address "0x744d70fdbe2ba4cf95131626614a1763df805b9e"
|
||||
:name "Status Network Token"
|
||||
:symbol :SNT
|
||||
:decimals 18
|
||||
:chainId 1}})
|
||||
|
||||
(h/deftest-sub :balances
|
||||
[sub-name]
|
||||
(swap! rf-db/app-db assoc
|
||||
:multiaccount/accounts accounts
|
||||
:wallet wallet)
|
||||
(is (= [{:ETH money-eth
|
||||
:SNT money-snt}
|
||||
{:ETH money-eth
|
||||
:SNT money-snt}]
|
||||
(rf/sub [sub-name]))))
|
||||
|
||||
(h/deftest-sub :wallet/token->decimals
|
||||
[sub-name]
|
||||
(swap! rf-db/app-db assoc :wallet/all-tokens tokens)
|
||||
(is (= {:SNT 18 :ETH 18}
|
||||
(rf/sub [sub-name]))))
|
||||
|
||||
(deftest get-balance-total-value-test
|
||||
(is (= 697.53
|
||||
(wallet/get-balance-total-value
|
||||
{:ETH (money/bignumber 1000000000000000000)
|
||||
:SNT (money/bignumber 100000000000000000000)
|
||||
:AST (money/bignumber 10000)}
|
||||
{:ETH {:USD {:from "ETH" :to "USD" :price 677.91 :last-day 658.688}}
|
||||
:SNT {:USD {:from "SNT" :to "USD" :price 0.1562 :last-day 0.15}}
|
||||
:AST {:USD {:from "AST" :to "USD" :price 4 :last-day 3}}}
|
||||
:USD
|
||||
{:ETH 18
|
||||
:SNT 18
|
||||
:AST 4}))))
|
||||
|
||||
(h/deftest-sub :portfolio-value
|
||||
[sub-name]
|
||||
(testing "returns fallback value when balances and prices are not available"
|
||||
(is (= "..." (rf/sub [sub-name]))))
|
||||
|
||||
(testing "returns zero when balance is not positive"
|
||||
(let [empty-wallet {:accounts {main-account-id
|
||||
{:balance {:ETH money-zero
|
||||
:SNT money-zero}}}}]
|
||||
(swap! rf-db/app-db assoc
|
||||
:multiaccount/accounts accounts
|
||||
:prices prices
|
||||
:wallet empty-wallet
|
||||
:wallet/all-tokens tokens)
|
||||
(is (= "0" (rf/sub [sub-name])))))
|
||||
|
||||
(testing "returns formatted value in the default USD currency"
|
||||
(swap! rf-db/app-db assoc
|
||||
:multiaccount/accounts accounts
|
||||
:prices prices
|
||||
:wallet wallet
|
||||
:wallet/all-tokens tokens)
|
||||
(is (= "20,550.76" (rf/sub [sub-name])))))
|
||||
|
||||
(h/deftest-sub :account-portfolio-value
|
||||
[sub-name]
|
||||
(testing "returns fallback value when balances and prices are not available"
|
||||
(is (= "..." (rf/sub [sub-name]))))
|
||||
|
||||
(testing "returns zero when balance is not positive"
|
||||
(let [empty-wallet {:accounts {main-account-id
|
||||
{:balance {:ETH money-zero
|
||||
:SNT money-zero}}}}]
|
||||
(swap! rf-db/app-db assoc
|
||||
:multiaccount/accounts accounts
|
||||
:prices prices
|
||||
:wallet empty-wallet
|
||||
:wallet/all-tokens tokens)
|
||||
(is (= "0" (rf/sub [sub-name main-account-id])))))
|
||||
|
||||
(testing "returns formatted value in the default USD currency"
|
||||
(swap! rf-db/app-db assoc
|
||||
:multiaccount/accounts accounts
|
||||
:prices prices
|
||||
:wallet wallet
|
||||
:wallet/all-tokens tokens)
|
||||
(is (= "10,275.38" (rf/sub [sub-name main-account-id])))))
|
Loading…
Reference in New Issue