Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make integration tests more enjoyable to use #19025

Merged
merged 14 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clj-kondo/config.edn
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
59 changes: 31 additions & 28 deletions .zprintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
169 changes: 166 additions & 3 deletions src/test_helpers/integration.cljs
Original file line number Diff line number Diff line change
@@ -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!
[]
Expand Down Expand Up @@ -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))
clauxx marked this conversation as resolved.
Show resolved Hide resolved
(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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While trying to use async fixtures I remembered of your suggestion in one of our Discord channels. Thanks @ulisesmac

"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.
clauxx marked this conversation as resolved.
Show resolved Hide resolved

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))})
Loading