Since the tested UI runs in a browser in a separate process from the
test code, there is no quarantee that the UI is immediately in certain
state after each test action. Not taking this into account will definitely
result in flaky indeterminisic tests. This is a universal problem not only
related to cuic
but other UI testing libraries as well.
Some libraries try to mitigate the async issues by introducing their
own DSLs that combine actions, data reading and assertions into chains
or similar constructs. cuic
takes entirely different approach. Unlike
in JavaScript, IO doesn't need to be asynchronous in JVM languages.
cuic
takes advance of this: each function call blocks if there are
any asynchronous activity and return data structures that can be
interacted with other (possibly blocking) functions and standard Clojure
language features. This makes tests straightforward and easy to debug.
Want to check that saving added rows to your db? Just make a db query
after (c/click save-btn)
and assert that the saved data is what you
expect!
cuic
has three levels to deal with asynchrony:
c/find
always waits until the searched element exists in DOMc/wait
macro allows waiting for any condition, blocking the execution
until the expected condition is satisfiedcuic.core/find
Based on the experience, in 90% of the cases, you're gonna lookup just one
specific element at time: "click save button", "fill email field", "type
xyz to the search box", "check that cancel button is disabled". Because
this behaviour is so common, single element lookup has special semantics
in cuic
: when you're searching for the element, cuic
expects it to
exist and tries its best to find the element before giving up. If the
element is not found immediately, cuic
waits a little and tries again
and again until the element appears in DOM or timeout exceeds. In case of
timeout, an exception is thrown. This means that find
always returns
an element that exists in DOM.
(let [save-btn (c/find "#save-btn")]
;; Save-button is now a handle to the actual HTML element **in** page
...)
Pay attention that element returned by find
is a handle to the
actual DOM node, not an abstract "description" how to find the node.
In other words, if the DOM node gets removed (either by user actions
or some background task), the handle becomes stale as well. Store
the element handles only the time you need them and discard them
immediately after that.
Element lookups are relatively cheap operations. Usually it's better to use functions to get element on demand. This also makes your tests cleaner because the function hides implementation details such as css selectors.
;;;;; try to avoid the following code
(let [save-btn (c/find "#app .footer button.save")]
(c/click save-btn)
;; ...do something else...
(c/click save-btn))
;;;;; instead, use function to defer the element lookup
(defn save-button []
(c/find "#app .footer button.save"))
(c/click (save-button))
;; ...do something else...
(c/click (save-button))
Separating element queries from actions is an intentional decision: query ensures that the queried element exists. It does not test any other conditions (such as that element is visible). However, if you want to interact with the element, certain other conditions must be satisfied as well before the action can be performed. For example, if you want to click button, the button must be visible in the viewport and enabled. Hovering, in the other hand, can be done even if button is not enabled.
Every built-in action in cuic
defines its own pre-conditions and waits
for them automatically if necessary. All of this is done by cuic
so
the only thing you need to do is to wait until action call returns.
Like with find
, returning from action means that the action was
performed successfully - any failures on pre-conditions will cause
an exception to be thrown.
(c/click (save-button))
;; here we can expect that save button **was** clicked
cuic
can guarantee that the action is peformed before the function
call returns. Hovever, it can't guarantee that the changes caused
by action are rendered to the DOM synchronously. Remember to use
cuic
's async primitives consistently - usage of find
and lazy
node lookups everywhere, after actions as well.
cuic.core/wait
- swiss army knife for any other situationThe core principle of cuic
is to avoid macros whenever possible.
However, when everything else fails, wait
macro will gonna save
your day. It allows you to wait for any condition. The contract is
simple: (c/wait expr)
waits for expr
to return a truthy value
and then returns the value. If expr
returns a falsy value, wait
will retry the same expression until it becomes non-falsy or
timeouts (in which case a timeout exception is thrown). Note that
because wait
may run the given expr
multiple times, it's
extremely important that expr
does not have any side effects.
Never put an action inside wait
.
Once you learn how to use wait
, you can make practically any
custom action, lookup or assertion your project needs:
;; Assertions
(is (c/wait (c/visible? (save-button))))
(is (c/wait (c/has-class? (save-button) "primary")))
(is (c/wait (re-find #"Changes saved" (c/inner-text (save-summary)))))
;; Lookup button by text
(defn button-by-text [text]
(c/wait (->> (c/query "button")
(filter #(string/includes? (c/text-content %) text))
(first))))
(c/click (button-by-text "Save"))
;; Or even custom action!
(defn ogy-click [button-text]
(c/click (button-by-text button-text)))
(ogy-click "Save")
And the coolest thing is that the waited expression doesn't even
need be related to UI at all! Wan't to check that saved data is
found from db? Use wait
to check the expceted value:
(add-todo "Foo")
(add-todo "Bar")
(ogy-click "Save")
;; Note that save request might take some time so the added todo
;; items are not found from the db immediately => wait
(is (c/wait (= #{"Foo" "Bar"} (set (map :text (query (get-db-conn) "SELECT text FROM todos"))))))
Cuic elements are not thread safe. Do not test your luck and try to use single element from multiple threads. It's ok to parallelize entire test cases though.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close