Equality checks in tests using = give a bad experience by default on test
failures containing nested data structures. We use the cljs.test directive
match? from matcher-combinators library to help compare nested structures. The
problem with match? is that its default matcher for maps (embeds) can be too
permissive, and this causes surprises.
Here we upgrade matcher-combinators to latest, where a new matcher called
nested-equals is available. This matcher won't allow extra keys in maps. This
matcher eliminates the need for manually adding nested equals matchers as we
have to do currently.
- Upgrades matcher-combinators from 3.8.8 to 3.9.1 (latest as of 2024-07-19)
What changes?
When asserting in tests, we now have the option to use match-strict? or match?.
Both directives are available by integrating with cljs.test. The code
implementing the new match-strict? directive was 100% copied from the library
matcher-combinators because we need to wrap the expected value ourselves with
matcher-combinators.matchers/nested-equals. It's ugly code, but it's how we can
integrate with cljs.test/assert-expr.
Introduces a new macro deftest-event to facilitate writing tests for event
handlers. Motivation came from the _problem of having to always extract event
handlers as vars in order to test them_.
Although the implementation of deftest-sub and deftest-event are similar,
deftest-sub is critically important because it guarantees changes in one
subscription can be caught by tests from all other related subscriptions in the
graph (reference: PR https://github.com/status-im/status-mobile/pull/14472).
This is not the case for the new deftest-event macro. deftest-event is
essentially a way of make testing events less ceremonial by not requiring event
handlers to be extracted to vars. But there are a few other small benefits:
- The macro uses re-frame and "finds" the event handler by computing the
interceptor chain (except :do-fx), so in a way, the tests are covering a bit
more ground.
- Slightly easier way to find event tests in the repo since you can just find
references to deftest-event.
- Possibly slightly easier to maintain by devs because now event tests and sub
tests are written in a similar fashion.
- Less code diff. Whether an event has a test or not, there's no var to
add/remove.
- The dispatch function provided by the macro makes reading the tests easier
over time. For example, when we read subscription tests, the Act section of
the test is always the same (rf/sub [sub-name]). Similarly for events, the
Act section is always (dispatch [event-id arg1 arg2]).
- Makes the re-frame code look more idiomatic because it's more common to define
handlers as anonymous functions.
Downside: deftest-sub and deftest-event are relatively complicated macros.
Note: The test suite runs just as fast and clj-kondo can lint code within the
macro just as well.
Before:
```clojure
(deftest process-account-from-signal-test
(testing "process account from signal"
(let [cofx {:db {:wallet {:accounts {}}}}
effects (events/process-account-from-signal cofx [raw-account])
expected-effects {:db {:wallet {:accounts {address account}}}
:fx [[:dispatch [:wallet/get-wallet-token-for-account address]]
[:dispatch
[:wallet/request-new-collectibles-for-account-from-signal address]]
[:dispatch [:wallet/check-recent-history-for-account address]]]}]
(is (match? expected-effects effects)))))
```
After
```clojure
(h/deftest-event :wallet/process-account-from-signal
[event-id dispatch]
(let [expected-effects
{:db {:wallet {:accounts {address account}}}
:fx [[:dispatch [:wallet/get-wallet-token-for-account address]]
[:dispatch [:wallet/request-new-collectibles-for-account-from-signal address]]
[:dispatch [:wallet/check-recent-history-for-account address]]]}]
(reset! rf-db/app-db {:wallet {:accounts {}}})
(is (match? expected-effects (dispatch [event-id raw-account])))))
```
Preload the user namespace (src/user.cljs and src/dev/user.cljs) for the mobile
target and for dev-only purposes. The files are git-ignored.
Just a reminder that you'll be responsible for making sure your user namespace
is correct. If it's broken in any way (e.g. calling non-existent code) the app
will crash at initialization (dev-only environment obviously).
Why? When the app initializes, it loads namespaces that were required at least
once. If you create a user namespace, it won't be automatically required for
you. And if you, like some Clojure devs, like to use the user namespace as
your safe heaven for experimentation and dev-only utilities, you'll need to
remember to evaluate the namespace at least once.
This is tedious and many times I forgot to do so and the app crashed because the
compiler didn't know where the symbols were coming from.
This PR upgrades clj-kondo (our Clojure linter) from v2023-09-07 to
v2024-03-13, so ~6 months of development updates.
You can check out the changes starting at
https://github.com/clj-kondo/clj-kondo/blob/master/CHANGELOG.md#20231020.
Nothing terribly useful to us this time, but as usual, clj-kondo can catch
more problems and more reliably than before.
This commit brings numerous improvements to integration tests. The next step
will be to apply the same improvements to contract tests.
Fixes https://github.com/status-im/status-mobile/issues/18676
Improvements:
- Setting up the application and logged account per test is now done with an
async test fixture, which is a very idiomatic way to solve this problem. No
need anymore to write macros to wrap day8.re-frame.test/wait-for. The macros
in test-helpers.integration will be removed once we apply the same
improvements to contract tests.
- Integration test timeouts can be controlled per test, with a configurable,
global default (60s).
- Now the integration test suite will fail-fast by default, i.e. a test failure
short-circuits the entire suite immediately. This option can be overridden on
a test-by-test basis. This improvement is very useful when investigating
failures because the error will be shown on the spot, with no need to search
backwards across lots of logs.
- Noisy messages from re-frame can be silenced with a test fixture. We can
silence even more in the future if we remove the hardcoded printf call from
C++ on every signal and control it with Clojure. We can disable most logs as
well with the more direct (status-im.common.log/setup "ERROR") at the top of
tests.integration-test.core. We can make verbosity even more convenient to
control, but I think this should be designed outside this PR.
- Removed dependency on lib day8.re-frame/test for integration tests (see
detailed explanation below).
- Each call to (our) wait-for can customize the timeout to process re-frame
event IDs passed to it.
- Syntax is now flat, instead of being nested on every call to wait-for. You
can now compose other async operations anywhere in a test.
Notes:
- promesa.core/do is essential in the integration test suite, as it makes sync &
async operations play nice with each other without the developer having to
promisify anything manually.
- There are lots of logs related to async storage ("unexpected token u in JSON at
position..."). This isn't fixed yet.
Are we not going to use day8.re-frame.test?
We don't need this library in integration tests and we won't need it in contract
tests. Whether it will be useful after we remove it from integration and
contract tests is yet to be seen (probably not).
A few reasons:
- The async model of promises is well understood and battle tested. The one
devised in the lib is poorly understood and rigid.
- There's basically no way to correctly integrate other async operations in the
test, unless they can be fully controlled via re-frame events. For instance,
how would you control timeouts? How would you retry in a test? How would
forcefully delay an operation by a few seconds? These things are useful (to me
at least) when developing integration/contract tests.
- Every call to day8.re-frame.test/wait-for forces you to nest code one more
level. Code readability suffers from that choice.
- Have you ever looked up the implementation of wait-for? It's quite convoluted.
One could say the source code is not that important, but many times I had to
look it up because I couldn't understand the async model they built with their
macro approach. The de facto primitive in JS for asynchronicity is promises,
and we fully leverage it in this PR.
- The lib has an interesting macro run-test-sync, but we have no usage for it. I
used it in status-mobile for a while. At one point, all event unit tests for
the Activity Center used it (e.g. commit
08fb0de7b0), but I replaced them with the
simpler pure function style.
Problem: failed equality checks as in "(is (= expected actual))" will give a
single, long line of output that for anything but the simplest data structures
is unreadable by humans, and the output doesn't give a useful diff.
Solution: use library https://github.com/nubank/matcher-combinators and its test
directive "match?" which will pinpoint where two data structures differ. Then,
instead of "(is (= ...", use "(is (match? expected actual)". It works
beautifully.
The library offers other nice matchers, but the majority of the time match? is
sufficient.
Can we use another test runner like Kaocha? kaocha-cljs2
(https://github.com/lambdaisland/kaocha-cljs2) would be able to print better
test errors out of the box, among other features, but I have no clue if it would
work well or at all in our stack (in theory yes, but it's a larger piece of
work).
This commit is the foundational step to start using malli
(https://github.com/metosin/malli) in this project.
Take in consideration we will only be able to realize malli's full power in
future iterations.
For those without context: the mobile team watched a presentation about malli
and went through a light RFC to put everyone on the same page, among other
discussions here and there in PRs.
To keep things relatively short:
1. Unit, integration and component tests will short-circuit (fail) when
inputs/outputs don't conform to their respective function schemas (CI should
fail too).
2. Failed schema checks will not block the app from initializing, nor throw an
exception that would trigger the LogBox. Exceptions are only thrown in the
scope of automated tests.
3. There's zero performance impact in production code because we only
instrument. Instrumentation is removed from the compiled code due to the
usage of "^boolean js.goog/DEBUG".
4. We shouldn't expect any meaningful slowdown during development.
**What are we instrumenting in this PR?**
Per our team's agreement, we're only instrumenting the bare minimum to showcase 2 examples.
- Instrument a utility function utils.money/format-amount using the macro
approach.
- Instrument a quo component quo.components.counter.step.view/view using the
functional approach.
Both approaches are useful, the functional approach is powerful and allow us to
instrument anonymous functions, like the ones we pass to subscriptions or event
handlers, or the higher-order function quo.theme/with-theme. The macro approach
is perfect for functions already defined with defn.
**I evaluated the schema or function in the REPL but nothing changes**
- If you evaluate the source function, you need to evaluate schema/=> or
schema/instrument as well.
- Remember to *var quote* when using schema/instrument.
- You must call "(status-im2.setup.schema/setup!)" after any var is
re-instrumented. It's advisable to add a keybinding in your editor to send
this expression automatically to the CLJS REPL, or add the call at the end of
the namespace you are working on (similar to how some devs add "(run-tests)"
at the end of test namespaces).
**Where should schemas be defined?**
For the moment, we should focus on instrumenting quo components, so define each
function schema in the same namespace as the component's public "view" var.
To be specific:
- A schema used only to instrument a single function and not used elsewhere,
like a quo component schema, wouldn't benefit from being defined in a separate
namespace because that would force the developer to constantly open two files
instead of one to check function signatures.
- A common schema reused across the repo, like ":schema.common/theme" should be
registered in the global registry "schema.registry" so that consumers can just
refer to it by keyword, as if it was a built-in malli schema.
- A common schema describing status-go entities like message, notification,
community, etc can be stored either in the respective
"src/status_im2/contexts/*" or registered globally, or even somewhere else.
This is yet to be defined, but since I chose not to include schemas for them,
we can postpone this guideline.
This commit adds a custom linter to verify i18n/label is called with a qualified
keyword, like :t/foo. More sophisticated linters are possible too.
We also set the stage for other developers to consider more lint automation
instead of manually reviewing conventions in PRs.
If you want to understand how to write custom linters, check out
https://github.com/clj-kondo/clj-kondo/blob/master/doc/hooks.md. You can fire
the Clojure JVM REPL in status-mobile and play with the clj-kondo hook too, it
works beautifully.
Why do we care? By making sure all translation keywords are qualified with "t",
it is trivial to grep or replace them because they're unique in the repo, and
can't be confused with other words if you search by ":t/<something>".
Note: It's a best practice to commit clj-kondo configuration from external
libraries in the .clj-kondo directory. The directory .clj-kondo/babashka is
auto-generated, that's why it was added.
Recently, we changed clj-kondo default fail-level from "warning" to "error", but
we missed the fact that we needed to raise the default level for all linters set
to "warning".
Unshadows all remaining vars in status-mobile, including non
cljs.core/clojure.core ones. The only exceptions are cljs.core/type and
cljs.core/name (which happen quite often, so I'm not sure if it's worth
unshadowing them).
This is a continuation of https://github.com/status-im/status-mobile/pull/16500 (Lint
& fix some shadowed core Clojure(Script) vars).
Notes: As a reminder, the goal is to eventually disallow shadowing core Clojure
vars entirely, but to get there and avoid rebase hell and regressions, we need
to do in smaller steps, especially because we can't safely automate the process
of unshadowing vars.
We are already down from ~500 shadowed core vars to 350 in total.
Why is this PR is using names such as "s", "v" or "sym"? Names such as s or v
are the so called idiomatic names, and are listed in the Clojure Style Guide
https://guide.clojure.style/#idiomatic-names. I used them whenever I felt
appropriate. For the var cljs.core/symbol I opted to use sym, even though the
symbol in question is not necessarily a Clojure symbol, I think the alias
conveys the meaning well enough
(https://www.clojure.org/guides/learn/syntax#_symbols_and_idents).
New vars linted:
- comparator
- identity
- str
- symbol
- val
Outstanding shadowed vars include type, name, hash, comp.
It's well known that shadowing core Clojure vars can lead to unexpected bugs. In
fact, it's a common source of bugs in other languages too. In the status-mobile
repository there are, in total, 562 shadowed vars, ~500 are core vars. Excluding
the "old code" we still have 285 offenders.
In status-mobile I've already seen two bugs caused by shadowed vars, both with
the shadowed var "name". But probably other problems happened in the past, and
others will happen in the future if we don't do something about this. This PR is
also my response to my frustration trying to review PRs and checking for
shadowed vars, humans were not meant for that!
In this commit we are enabling ":shadowed-var" to lint certain (not all) core
vars as errors (not warnings). In future PRs we can gradually unshadow more
vars. For the record, name is shadowed 40 times in the new code and 130 in
total, and type is shadowed 93 times in the new code and 124 in total!
What about non-core vars, should we allow shadowing? There are ~70 non-core
shadowed vars. In my opinion, we should also lint and disallow shadowing
non-core vars, since it may cause the same kind of bugs of shadowing core vars.
But this decision can be left for another moment/issue, after we have fixed the
most prominent problem of shadowing core vars.
Which vars are unshadowed in this PR? I fixed 62 errors and unshadowed
cljs.core/iter, cljs.core/time, cljs.core/count, cljs.core/key,
clojure.core/key.
Resources:
- [clj-kondo linter: shadowed-var](https://github.com/clj-kondo/clj-kondo/blob/master/doc/linters.md#shadowed-var)
EXPERIMENTAL: uses reanimated lib so we can use reanimated buttons inside and have simultaneous handlers
Add react hooks
Use hooks
mocks
Use timing for drag transition
Use view on android
Signed-off-by: Gheorghe Pinzaru <feross95@gmail.com>