diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index a0c50c61b2..cc5945d3aa 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -26,6 +26,7 @@ malli.generator malli.generator malli.transform malli.transform malli.util malli.util + promesa.core p schema.core schema status-im.feature-flags ff taoensso.timbre log}} diff --git a/.zprintrc b/.zprintrc index 142d3d0015..5a809dfcef 100644 --- a/.zprintrc +++ b/.zprintrc @@ -22,34 +22,37 @@ :multi-lhs-hang] :fn-map - {"reg-sub" :arg1-pair - "h/describe" :arg1-body - "h/describe-skip" :arg1-body - "h/describe-only" :arg1-body - "h/test" :arg1-body - "h/test-skip" :arg1-body - "h/test-only" :arg1-body - "global.describe" :arg1-body - "global.test" :arg1-body - "list-comp" :binding - "defview" :arg1-body - "letsubs" :binding - "with-let" "let" - "reg-event-fx" :arg1-pair - "reg-fx" :arg1-pair - "testing" :arg1-body - "deftest-sub" :arg1-body - "wait-for" :arg1-body - "with-deps-check" :arg1-body - "schema/=>" :arg1-body - "->" [:noarg1-body - {:list {:constant-pair? false :force-nl? false} - :next-inner-restore [[:list :constant-pair?]]}] - "set!" "reset!" - "assoc-when" "assoc" - "assoc-some" "assoc" - "conj-when" "conj" - "conj-some" "conj"} + {"reg-sub" :arg1-pair + "h/describe" :arg1-body + "h/describe-skip" :arg1-body + "h/describe-only" :arg1-body + "h/test" :arg1-body + "h/test-skip" :arg1-body + "h/test-only" :arg1-body + "test/async" :arg1-body + "test/use-fixtures" :arg1-body + "global.describe" :arg1-body + "global.test" :arg1-body + "list-comp" :binding + "defview" :arg1-body + "letsubs" :binding + "with-let" "let" + "reg-event-fx" :arg1-pair + "reg-fx" :arg1-pair + "testing" :arg1-body + "deftest-sub" :arg1-body + "h/integration-test" :arg1-body + "wait-for" :arg1-body + "with-deps-check" :arg1-body + "schema/=>" :arg1-body + "->" [:noarg1-body + {:list {:constant-pair? false :force-nl? false} + :next-inner-restore [[:list :constant-pair?]]}] + "set!" "reset!" + "assoc-when" "assoc" + "assoc-some" "assoc" + "conj-when" "conj" + "conj-some" "conj"} :style-map {:no-comma {:map {:comma? false}} diff --git a/src/test_helpers/integration.cljs b/src/test_helpers/integration.cljs index 36f0ef4881..ff76b628fd 100644 --- a/src/test_helpers/integration.cljs +++ b/src/test_helpers/integration.cljs @@ -1,16 +1,35 @@ (ns test-helpers.integration (:require-macros [test-helpers.integration]) (:require - [cljs.test :refer [is]] + [cljs.test :refer [is] :as test] legacy.status-im.events + [legacy.status-im.multiaccounts.logout.core :as logout] legacy.status-im.subs.root + [legacy.status-im.utils.test :as legacy-test] [native-module.core :as native-module] + [promesa.core :as p] [re-frame.core :as rf] + [re-frame.interop :as rf.interop] status-im.events status-im.navigation.core status-im.subs.root [taoensso.timbre :as log] - [tests.integration-test.constants :as constants])) + [tests.integration-test.constants :as constants] + [utils.collection :as collection])) + +(def default-re-frame-wait-for-timeout-ms + "Controls the maximum time allowed to wait for all events to be processed by + re-frame on every call to `wait-for`. + + Take into consideration that some endpoints/signals may take significantly + more time to finish/arrive." + (* 10 1000)) + +(def default-integration-test-timeout-ms + "Use a high-enough value in milliseconds to timeout integration tests. Not too + small, which would cause sporadic failures, and not too high as to make you + sleepy." + (* 60 1000)) (defn initialize-app! [] @@ -59,4 +78,148 @@ (defn log-headline [test-name] - (log/info (str "========= " (name test-name) " =================="))) + (log/info (str "==== " test-name " ===="))) + +(defn wait-for + "Returns a promise that resolves when all `event-ids` are processed by re-frame, + otherwise rejects after `timeout-ms`. + + If an event ID that is expected in `event-ids` occurs in a different order, + the promise will be rejected." + ([event-ids] + (wait-for event-ids default-re-frame-wait-for-timeout-ms)) + ([event-ids timeout-ms] + (let [waiting-ids (atom event-ids)] + (p/create + (fn [promise-resolve promise-reject] + (let [cb-id (gensym "post-event-callback") + timer-id (js/setTimeout (fn [] + (rf/remove-post-event-callback cb-id) + (promise-reject (ex-info + "timed out waiting for all event-ids to run" + {:event-ids event-ids + :waiting-ids @waiting-ids + :timeout-ms timeout-ms} + ::timeout))) + timeout-ms)] + (rf/add-post-event-callback + cb-id + (fn [[event-id & _]] + (when-let [idx (collection/first-index #(= % event-id) @waiting-ids)] + ;; All `event-ids` should be processed in their original order. + (if (zero? idx) + (do + (swap! waiting-ids rest) + ;; When there's nothing else to wait for, clean up resources. + (when (empty? @waiting-ids) + (js/clearTimeout timer-id) + (rf/remove-post-event-callback cb-id) + (promise-resolve))) + (do + (js/clearTimeout timer-id) + (rf/remove-post-event-callback cb-id) + (promise-reject (ex-info "event happened in unexpected order" + {:event-ids event-ids + :waiting-for @waiting-ids} + ::out-of-order-event-id))))))))))))) + +(defn setup-app + [] + (legacy-test/init!) + (if (app-initialized) + (p/resolved ::app-initialized) + (do + (rf/dispatch [:app-started]) + (wait-for [:profile/get-profiles-overview-success])))) + +(defn setup-account + [] + (if (messenger-started) + (p/resolved ::messenger-started) + (do + (create-multiaccount!) + (-> (wait-for [:messenger-started]) + (.then #(assert-messenger-started)))))) + +(defn integration-test + "Runs `f` inside `cljs.test/async` macro in a restorable re-frame checkpoint. + + `f` will be called with one argument, the `done` function exposed by the + `cljs.test/async` macro. Normally, you don't need to use `done`, but you can + call it if you want to early-terminate the current test, so that the test + runner can execute the next one. + + Option `fail-fast?`, when truthy (defaults to true), will force the test + runner to terminate on any test failure. Setting it to false can be useful + during development when you want the rest of the test suite to run due to a + flaky test. Prefer to fail fast in the CI to save on time & resources. + + When `fail-fast?` is falsey, re-frame's state is automatically restored after + a test failure, so that the next integration test can run from a pristine + state. + + Option `timeout-ms` controls the total time allowed to run `f`. The value + should be high enough to account for some variability, otherwise the test may + fail more often. + " + ([test-name f] + (integration-test test-name + {:fail-fast? true + :timeout-ms default-integration-test-timeout-ms} + f)) + ([test-name {:keys [fail-fast? timeout-ms]} f] + (test/async + done + (let [restore-fn (rf/make-restore-fn)] + (log-headline test-name) + (-> (p/do (f done)) + (p/timeout timeout-ms) + (p/catch (fn [error] + (is (nil? error)) + (when fail-fast? + (js/process.exit 1)))) + (p/finally (fn [] + (restore-fn) + (done)))))))) + +;;;; Fixtures + +(defn fixture-session + "Fixture to set up the application and a logged account before the test runs. + Log out after the test is done. + + Usage: + + (use-fixtures :each (h/fixture-logged))" + [] + {:before (fn [] + (test/async done + (p/do (setup-app) + (setup-account) + (done)))) + :after (fn [] + (test/async done + (p/do (logout) + (wait-for [::logout/logout-method]) + (done))))}) + +(defn fixture-silence-reframe + "Fixture to disable most re-frame messages. + + Avoid using this fixture for non-dev purposes because in the CI output it's + desirable to have more data to debug, not less. + + Example messages disabled: + + - Warning about subscriptions being used in non-reactive contexts. + - Debug message \"Handling re-frame event: XYZ\". + + Usage: + + (use-fixtures :once (h/fixture-silence-re-frame)) + " + [] + {:before (fn [] + (set! rf.interop/debug-enabled? false)) + :after (fn [] + (set! rf.interop/debug-enabled? true))}) diff --git a/src/tests/integration_test/chat_test.cljs b/src/tests/integration_test/chat_test.cljs index ce3e722f9b..688f974859 100644 --- a/src/tests/integration_test/chat_test.cljs +++ b/src/tests/integration_test/chat_test.cljs @@ -1,10 +1,9 @@ (ns tests.integration-test.chat-test (:require - [cljs.test :refer [deftest is]] - [day8.re-frame.test :as rf-test] + [cljs.test :refer [deftest is use-fixtures]] legacy.status-im.events - [legacy.status-im.multiaccounts.logout.core :as logout] legacy.status-im.subs.root + [promesa.core :as p] [re-frame.core :as rf] [status-im.constants :as constants] status-im.events @@ -12,85 +11,69 @@ status-im.subs.root [test-helpers.integration :as h])) +(use-fixtures :each (h/fixture-session)) + (def chat-id "0x0402905bed83f0bbf993cee8239012ccb1a8bc86907ead834c1e38476a0eda71414eed0e25f525f270592a2eebb01c9119a4ed6429ba114e51f5cb0a28dae1adfd") (deftest one-to-one-chat-test - (h/log-headline :one-to-one-chat-test) - (rf-test/run-test-async - (h/with-app-initialized - (h/with-account - (rf/dispatch-sync [:chat.ui/start-chat chat-id]) ;; start a new chat - (rf-test/wait-for - [:chat/one-to-one-chat-created] - (rf/dispatch-sync [:chat/navigate-to-chat chat-id]) - (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))) - (h/logout) - (rf-test/wait-for [::logout/logout-method])))))) + (h/integration-test ::one-to-one-chat + (fn [] + (p/do + (rf/dispatch-sync [:chat.ui/start-chat chat-id]) + (h/wait-for [:chat/one-to-one-chat-created]) + (rf/dispatch-sync [:chat/navigate-to-chat chat-id]) + (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))))))) (deftest delete-chat-test - (h/log-headline :delete-chat-test) - (rf-test/run-test-async - (h/with-app-initialized - (h/with-account - (rf/dispatch-sync [:chat.ui/start-chat chat-id]) ;; start a new chat - (rf-test/wait-for - [:chat/one-to-one-chat-created] - (rf/dispatch-sync [:chat/navigate-to-chat chat-id]) - (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))) - (is @(rf/subscribe [:chats/chat chat-id])) - (rf/dispatch-sync [:chat.ui/show-remove-confirmation chat-id]) - (rf/dispatch-sync [:chat.ui/remove-chat chat-id]) - (h/logout) - (rf-test/wait-for [::logout/logout-method])))))) + (h/integration-test ::delete-chat + (fn [] + (p/do + (rf/dispatch-sync [:chat.ui/start-chat chat-id]) + (h/wait-for [:chat/one-to-one-chat-created]) + (rf/dispatch-sync [:chat/navigate-to-chat chat-id]) + (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))) + (is @(rf/subscribe [:chats/chat chat-id])) + (rf/dispatch-sync [:chat.ui/show-remove-confirmation chat-id]) + (rf/dispatch-sync [:chat.ui/remove-chat chat-id]))))) (deftest mute-chat-test - (h/log-headline :mute-chat-test) - (rf-test/run-test-async - (h/with-app-initialized - (h/with-account - (rf/dispatch-sync [:chat.ui/start-chat chat-id]) ;; start a new chat - (rf-test/wait-for - [:chat/one-to-one-chat-created] - (rf/dispatch-sync [:chat/navigate-to-chat chat-id]) - (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))) - (is @(rf/subscribe [:chats/chat chat-id])) - (rf/dispatch-sync [:chat.ui/mute chat-id true constants/mute-till-unmuted]) - (rf-test/wait-for - [:chat/mute-successfully] - (is @(rf/subscribe [:chats/muted chat-id])) - (rf/dispatch-sync [:chat.ui/mute chat-id false]) - (rf-test/wait-for - [:chat/mute-successfully] - (is (not @(rf/subscribe [:chats/muted chat-id]))) - (h/logout) - (rf-test/wait-for [::logout/logout-method])))))))) + (h/integration-test ::mute-chat + (fn [] + (p/do + (rf/dispatch-sync [:chat.ui/start-chat chat-id]) + (h/wait-for [:chat/one-to-one-chat-created]) + + (rf/dispatch-sync [:chat/navigate-to-chat chat-id]) + (is (= chat-id @(rf/subscribe [:chats/current-chat-id]))) + (is @(rf/subscribe [:chats/chat chat-id])) + + (rf/dispatch-sync [:chat.ui/mute chat-id true constants/mute-till-unmuted]) + (h/wait-for [:chat/mute-successfully]) + (is @(rf/subscribe [:chats/muted chat-id])) + + (rf/dispatch-sync [:chat.ui/mute chat-id false]) + (h/wait-for [:chat/mute-successfully]) + (is (not @(rf/subscribe [:chats/muted chat-id]))))))) (deftest add-contact-test - (h/log-headline :add-contact-test) - (let - [compressed-key "zQ3shMwgSMKHVznoowceZMxWde9HUnkQEVSGvvex8UFpFNErL" - public-key (str "0x0407e9dc435fe366cb0b4c4f35cbd925438c0f46fe0" - "ed2a86050325bc8856e26898c17e31dee2602b9429c91" - "ecf65a41d62ac1f2f0823c0710dcb536e79af2763c") - primary-name "zQ3...pFNErL"] - (rf-test/run-test-async - (h/with-app-initialized - (h/with-account - ;; search for contact using compressed key - (rf/dispatch [:contacts/set-new-identity {:input compressed-key}]) - (rf-test/wait-for - [:contacts/set-new-identity-success] - (let [new-identity @(rf/subscribe [:contacts/new-identity])] - (is (= public-key (:public-key new-identity))) - (is (= :valid (:state new-identity)))) - ;; click 'view profile' button - (rf/dispatch [:chat.ui/show-profile public-key]) - (rf-test/wait-for - [:contacts/build-contact] - (rf-test/wait-for - [:contacts/build-contact-success] - (let [contact @(rf/subscribe [:contacts/current-contact])] - (is (= primary-name (:primary-name contact)))) - (h/logout) - (rf-test/wait-for [::logout/logout-method]))))))))) + (h/integration-test ::add-contact + (fn [] + (let [compressed-key "zQ3shMwgSMKHVznoowceZMxWde9HUnkQEVSGvvex8UFpFNErL" + public-key (str "0x0407e9dc435fe366cb0b4c4f35cbd925438c0f46fe0" + "ed2a86050325bc8856e26898c17e31dee2602b9429c91" + "ecf65a41d62ac1f2f0823c0710dcb536e79af2763c") + primary-name "zQ3...pFNErL"] + (p/do + ;; Search for contact using compressed key + (rf/dispatch [:contacts/set-new-identity {:input compressed-key}]) + (h/wait-for [:contacts/set-new-identity-success]) + (let [new-identity @(rf/subscribe [:contacts/new-identity])] + (is (= public-key (:public-key new-identity))) + (is (= :valid (:state new-identity)))) + + ;; Click 'view profile' button + (rf/dispatch [:chat.ui/show-profile public-key]) + (h/wait-for [:contacts/build-contact :contacts/build-contact-success]) + (let [contact @(rf/subscribe [:contacts/current-contact])] + (is (= primary-name (:primary-name contact))))))))) diff --git a/src/tests/integration_test/core_test.cljs b/src/tests/integration_test/core_test.cljs index bc98c70703..0350ef28cf 100644 --- a/src/tests/integration_test/core_test.cljs +++ b/src/tests/integration_test/core_test.cljs @@ -1,11 +1,11 @@ (ns tests.integration-test.core-test (:require [cljs.test :refer [deftest]] - [day8.re-frame.test :as rf-test] legacy.status-im.events [legacy.status-im.multiaccounts.logout.core :as logout] legacy.status-im.subs.root [legacy.status-im.utils.test :as utils.test] + [promesa.core :as p] [re-frame.core :as rf] status-im.events status-im.navigation.core @@ -13,22 +13,22 @@ [test-helpers.integration :as h])) (deftest initialize-app-test - (h/log-headline :initialize-app-test) - (rf-test/run-test-async - (utils.test/init!) - (rf/dispatch [:app-started]) - (rf-test/wait-for - ;; use initialize-view because it has the longest avg. time and - ;; is dispatched by initialize-multiaccounts (last non-view event) - [:profile/get-profiles-overview-success] - (rf-test/wait-for - [:font/init-font-file-for-initials-avatar] - (h/assert-app-initialized))))) + (h/integration-test ::initialize-app + (fn [] + (p/do + (utils.test/init!) + (rf/dispatch [:app-started]) + ;; Use initialize-view because it has the longest avg. time and is + ;; dispatched by initialize-multiaccounts (last non-view event). + (h/wait-for [:profile/get-profiles-overview-success + :font/init-font-file-for-initials-avatar]) + (h/assert-app-initialized))))) (deftest create-account-test - (h/log-headline :create-account-test) - (rf-test/run-test-async - (h/with-app-initialized - (h/with-account - (h/logout) - (rf-test/wait-for [::logout/logout-method]))))) + (h/integration-test ::create-account + (fn [] + (p/do + (h/setup-app) + (h/setup-account) + (h/logout) + (h/wait-for [::logout/logout-method]))))) diff --git a/src/tests/integration_test/profile_test.cljs b/src/tests/integration_test/profile_test.cljs index 29c7bd0dbb..f7cf38310f 100644 --- a/src/tests/integration_test/profile_test.cljs +++ b/src/tests/integration_test/profile_test.cljs @@ -1,75 +1,52 @@ (ns tests.integration-test.profile-test (:require - [cljs.test :refer [deftest is]] - [day8.re-frame.test :as rf-test] - [legacy.status-im.multiaccounts.logout.core :as logout] + [cljs.test :refer [deftest is use-fixtures]] [legacy.status-im.utils.test :as utils.test] + [promesa.core :as p] [status-im.contexts.profile.utils :as profile.utils] [test-helpers.integration :as h] [utils.re-frame :as rf])) +(use-fixtures :each (h/fixture-session)) + (deftest edit-profile-name-test - (h/log-headline :edit-profile-name-test) - (let [new-name "John Doe"] - (rf-test/run-test-async - (h/with-app-initialized - (h/with-account - (rf/dispatch [:profile/edit-name new-name]) - (rf-test/wait-for - [:navigate-back] - (rf-test/wait-for - [:toasts/upsert] - (let [profile (rf/sub [:profile/profile]) - display-name (profile.utils/displayed-name profile)] - (is (= new-name display-name))) - (h/logout) - (rf-test/wait-for [::logout/logout-method])))))))) + (h/integration-test ::edit-profile-name + (fn [] + (let [new-name "John Doe"] + (p/do + (rf/dispatch [:profile/edit-name new-name]) + (h/wait-for [:navigate-back :toasts/upsert]) + (let [profile (rf/sub [:profile/profile]) + display-name (profile.utils/displayed-name profile)] + (is (= new-name display-name)))))))) (deftest edit-profile-picture-test - (h/log-headline :edit-profile-picture-test) - (let [mock-image "resources/images/mock2/monkey.png" - absolute-path (.resolve utils.test/path mock-image)] - (rf-test/run-test-async - (h/with-app-initialized - (h/with-account - (rf/dispatch [:profile/edit-picture absolute-path 80 80]) - (rf-test/wait-for - [:profile/update-local-picture] - (rf-test/wait-for - [:toasts/upsert] - (let [profile (rf/sub [:profile/profile])] - (is (not (nil? (:images profile))))) - (h/logout) - (rf-test/wait-for [::logout/logout-method])))))))) + (h/integration-test ::edit-profile-picture + (fn [] + (let [mock-image "resources/images/mock2/monkey.png" + absolute-path (.resolve utils.test/path mock-image)] + (p/do + (rf/dispatch [:profile/edit-picture absolute-path 80 80]) + (h/wait-for [:profile/update-local-picture :toasts/upsert]) + (let [profile (rf/sub [:profile/profile])] + (is (not (nil? (:images profile)))))))))) (deftest delete-profile-picture-test - (h/log-headline :delete-profile-picture-test) - (rf-test/run-test-async - (h/with-app-initialized - (h/with-account - (rf/dispatch [:profile/delete-picture]) - (rf-test/wait-for - [:profile/update-local-picture] - (rf-test/wait-for - [:toasts/upsert] - (let [profile (rf/sub [:profile/profile])] - (is (nil? (:image profile)))) - (h/logout) - (rf-test/wait-for [::logout/logout-method]))))))) + (h/integration-test ::delete-profile-picture + (fn [] + (p/do + (rf/dispatch [:profile/delete-picture]) + (h/wait-for [:profile/update-local-picture :toasts/upsert]) + (let [profile (rf/sub [:profile/profile])] + (is (nil? (:image profile)))))))) (deftest edit-profile-bio-test - (h/log-headline :edit-profile-bio-test) - (let [new-bio "New bio text"] - (rf-test/run-test-async - (h/with-app-initialized - (h/with-account - (rf/dispatch [:profile/edit-bio new-bio]) - (rf-test/wait-for - [:navigate-back] - (rf-test/wait-for - [:toasts/upsert] - (let [profile (rf/sub [:profile/profile]) - bio (:bio profile)] - (is (= new-bio bio))) - (h/logout) - (rf-test/wait-for [::logout/logout-method])))))))) + (h/integration-test ::edit-profile-bio + (fn [] + (let [new-bio "New bio text"] + (p/do + (rf/dispatch [:profile/edit-bio new-bio]) + (h/wait-for [:navigate-back :toasts/upsert]) + (let [profile (rf/sub [:profile/profile]) + bio (:bio profile)] + (is (= new-bio bio)))))))) diff --git a/src/utils/re_frame.cljs b/src/utils/re_frame.cljs index c35b5b08ea..babf23c915 100644 --- a/src/utils/re_frame.cljs +++ b/src/utils/re_frame.cljs @@ -3,6 +3,7 @@ (:require [re-frame.core :as re-frame] [re-frame.interceptor :as interceptor] + [re-frame.interop :as rf.interop] [reagent.core :as reagent] [taoensso.timbre :as log] [utils.datetime :as datetime]) @@ -20,7 +21,9 @@ [context] (when js/goog.DEBUG (reset! handler-nesting-level 0)) - (log/debug "Handling re-frame event: " (first (interceptor/get-coeffect context :event))) + (when rf.interop/debug-enabled? + (log/debug "Handling re-frame event: " + (first (interceptor/get-coeffect context :event)))) context))) (defn- update-db