Queries provide access to the Chrome page and its DOM. They're the essential
part of cuic
because every action and data access need an HTML element from
DOM. In cuic
, HTML elements are represented as data structures that contain
a handle to the actual page's DOM node. The most important queries are
cuic.core/find
and cuic.core/query
.
cuic.core/find
find
is the workhorse of cuic
. The majority of UI test use cases are
interactions with single element, such as "clicking the save button" or "checking
that the popup is open" hence find
provides a convenient way for finding single
element from the page. It works like JavaScript's document.querySelector
, taking
a valid CSS selector and returning an HTML element that matches the selector.
(let [save-btn (c/find "#save-btn")]
;; save-btn can be used for interactions
(is (= "Save!" (c/text-content save-btn))
(c/click save-btn))
(let [input (c/find "input[name='bio']")]
(c/fill input "tsers!"))
;; you can also use doto macro (or any other clojure stuff!)
(doto (c/find "input[name='bio']")
(c/fill "tsers!"))
Sometimes the target element may appear in DOM after some time (e.g. after a
successful AJAX request), thus find
tries to wait for the selector if the
element is not found immediately, but timeouts after a certain period.
(c/click (c/find "#save-btn"))
(is (= "Saved!" (c/text-content (c/find "#status-text"))))
find
expects exactly one element to be found. If the selector matches
multiple elements, find
throws an exception. If you need to find multiple
elements, see cuic.core/query
description below.
cuic.core/query
Whereas find
is meant for getting single html element, query
provides a way
to search for multiple elements matching the certain selector. Unlike find
,
it does not wait: if there are no elements matching the given selector at the time
of invocation, query
returns nil
. If there are some matching elements, those
elements are returned as a vector.
OBS! Pay special attention to the asynchrony when using query
. Because query
does not wait anything, it's extremely easy to make flaky tests by expecting something
from the query
result immediately. To mitigate this, query
usage should be combined
with cuic.core/wait
to eliminate the effects of asynchrony.
(defn todo-items []
(c/query ".todo-item"))
(is (= 0 (count (todo-items))))
(add-todo "tsers")
;; The following form has false positive possibility because DOM might not
;; have rendered fully at the invocation time of the assertion!
(is (= 1 (count (todo-items))))
;;; Using wait eliminates the problem
(is (= 0 (count (todo-items))))
(add-todo "tsers")
(is (c/wait (= 1 (count (todo-items)))))
;;;;;;;
(defn button-by-text [text]
(->> (c/query "button")
(filter #(string/includes? (c/text-content %) text))
(first))
(c/fill (c/find "#bio") "tsers!")
;; Undo button may not be rendered to the dom yet!
(c/click (button-by-text "Undo"))
;;; You can also use wait in your "utility" code to prevent unnecessary repetition
(defn button-by-text [text]
(try
(->> (c/query "button")
(filter #(string/includes? (c/text-content %) text))
(first)
(c/wait))
(catch Exception ex
(if (c/timeout-ex?? ex)
(throw (RuntimeException. (str "Could not find button by text: " text)))
(throw ex)))))
(c/fill (c/find "#bio") "tsers!")
(c/click (button-by-text "Undo")) ;; safe again!
Eventually there will be cases where you want to access an element that can't
be identified uniquely, for example remove the second "todo item" by clicking
its remove button. Writing selectors such as .todo-item:nth-child(1) button.remove
quickly become a maintenance hell, and your tests start falling apart. Because
this is such a common problem, cuic
provides some built-in tools to handle it.
So far we've been using query
and find
by giving only the selector as a
parameter. Both functions, however, have an alternative invocation style that
provides a way to define a "context" that'll be used for the query. You can
think it like JavaScript element.querySelector
or element.querySelectorAll
equivalent.
(def todo-list (c/find ".todo-list")
;; Get only todo items that are **inside** the todo-list element
(def todo-items
(c/query {:by ".todo-item"
:in todo-list))
;; Get only the remove button that is **inside** the second todo item
(c/find {:by "button.remove"
:in (second todo-items)}
However, when the application and the test cases become more complex, passing the context element around starts to produce boilerplate and hinder the maintainability of your tests.
(defn input-by-placeholder [ctx text]
(try
(->> (c/query {:in ctx :by "input, textarea"})
(filter #(= text (:placeholder (c/attributes %))))
(first)
(c/wait))
(catch Exception ex
(if (c/timeout? ex)
(throw (RuntimeException. (str "Could not find input by: " text)))
(throw ex)))))
(defn fill-text [ctx placeholder text]
(c/fill (input-by-placeholder ctx placeholder) text))
(fill-text (c/document) "Search..." "Matti"))
(c/press 'Enter)
(let [user (c/wait (first (c/query ".users")))]
(fill-text user "Name" "Pekka"))
(fill-text user "Bio" "tsers!")))
To mitigate this, cuic
provides the in
macro which setups the default context
for find
and query
. This makes possible to build more modular project/domain
specific utility functions:
(defn input-by-placeholder [text]
(try
(->> (c/query "input, textarea")
(filter #(= text (:placeholder (c/attributes %))))
(first)
(c/wait))
(catch Exception ex
(if (c/timeout? ex)
(throw (RuntimeException. (str "Could not find input by: " text)))
(throw ex)))))
(defn fill-text [placeholder text]
(c/fill (input-by-placeholder placeholder) text))
(fill-text "Search..." "Matti")) ;; document used by default
(c/press 'Enter)
(c/in (c/wait (first (c/query ".users"))) ;; use the first user as context inside this block
(fill-text "Name" "Pekka"))
(fill-text "Bio" "tsers!")))
Naming elements is entirely optional, although it greatly improves the debugging
in case of failures. If, for example, the clicked element is not visible, cuic
throws an exception using element's selector in the error message. If selector is
complex or queried from the context (see above), the error message might be hard
to interpret from e.g. CI output.
(defn button-by-text [text]
(->> (c/query "button")
(filter #(string/includes? (c/text-content %) text))
(first)
(c/wait)))
;; If save button is disabled the following exception is thrown:
;; => Can't click element "button" because it is not visible
(c/click (button-by-text "Save"))
;;;;;
(defn button-by-text [text]
(->> (c/query {:by "button"
;; Assign name to the queried elements
:as (str text " button")})
(filter #(string/includes? (c/text-content %) text))
(first)
(c/wait)))
;; => Can't click element "Save button" because it is not visible
(c/click (button-by-text "Save"))
Both find
and query
support :as <name>
option. Element can also be
renamed with c/as
and the assigned name is retrievable in code with c/name
.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close