-
Notifications
You must be signed in to change notification settings - Fork 4
/
test.clj
255 lines (224 loc) · 9.53 KB
/
test.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
(ns cuic.test
"Utilities for writing concise and robust UI tests.
Example usage:
```clojure
(ns todomvc-tests
(:require [clojure.test :refer :all]
[cuic.core :as c]
[cuic.test :refer [deftest* is* browser-test-fixture]]))
(use-fixtures
:once
(browser-test-fixture))
(defn todos []
(->> (c/query \".todo-list li\")
(map c/text-content)))
(defn add-todo [text]
(doto (c/find \".new-todo\")
(c/fill text))
(c/press 'Enter))
(deftest* creating-new-todos
(c/goto \"http://todomvc.com/examples/react\")
(is* (= [] (todos)))
(add-todo \"Hello world!\")
(is* (= [\"Hello world!\"] (todos)))
(add-todo \"Tsers!\")
(is* (= [\"Hello world!\" \"Tsers!\"] (todos))))
```"
(:require [clojure.test :refer [deftest is assert-expr do-report testing-contexts-str]]
[clojure.string :as string]
[clojure.java.io :as io]
[cuic.core :as c]
[cuic.chrome :as chrome])
(:import (java.io File)
(cuic TimeoutException)
(cuic.internal AbortTestError)))
(set! *warn-on-reflection* true)
(defonce ^:no-doc ^:dynamic *current-cuic-test* nil)
(defonce ^:private eventually
(gensym (str 'cuic.test/is-eventually-)))
(defmethod assert-expr eventually [msg [_ form]]
`(let [last-report# (atom nil)
result# (with-redefs [do-report #(reset! last-report# %)]
(try
{:value (c/wait ~(assert-expr msg form))}
(catch TimeoutException e#
(swap! last-report# #(or % {:type :fail
:message nil
:expected '~form
:actual (list '~'not '~form)}))
{:value (.getLatestValue e#)
:abort true
:timeout e#})
(catch Throwable t#
(reset! last-report# {:type :error
:message nil
:expected '~form
:actual t#})
{:abort true})))]
(when-let [ex# (:timeout result#)]
(do-report {:type :cuic/timeout
:message (ex-message ex#)
:expr '~form}))
(some-> @last-report# (do-report))
result#))
;;;;
(def ^:dynamic *screenshot-options*
"Options that [[cuic.test/deftest*]] adn [[cuic.test/is*]] will
use for screenshots. Accepts all options accepted by [[cuic.core/screenhot]]
plus `:dir` (`java.io.File` instance) defining directory, where the
taken screenshots should be saved.
```clojure
;; Store taken screenshots under $PWD/__screenshots__ directory
(use-fixtures
:once
(fn [t]
(binding [*screenshot-options* (assoc *screenshot-options* :dir (io/file \"__screenshots__\"))]
(t))))
```"
{:dir (io/file "target/screenshots")
:format :png
:timeout 10000})
(defn ^:no-doc -try-take-screenshot [test-name context-s]
(try
(when-let [browser c/*browser*]
(let [data (c/screenshot (assoc *screenshot-options* :browser browser))
dir (doto ^File (:dir *screenshot-options*)
(.mkdirs))]
(loop [base (-> (str (some-> test-name (string/replace #"\." "\\$")) context-s)
(string/lower-case)
(string/replace #"\s+" "-")
(string/replace #"[^a-z\-_0-9$]" ""))
i 0]
(let [n (str base (when (pos? i) (str "-" i)) "." (name (:format *screenshot-options*)))
f (io/file dir n)]
(if-not (.exists f)
(do (io/copy data f)
(do-report {:type :cuic/screenshot-taken
:filename (.getAbsolutePath f)}))
(recur base (inc i)))))))
(catch Exception ex
(do-report {:type :cuic/screenshot-failed
:cause ex}))))
(defn set-screenshot-options!
"Globally resets the default screenshot options. Useful for
example REPL usage. See [[cuic.test/*screenshot-options*]]
for more details."
[opts]
{:pre [(map? opts)]}
(alter-var-root #'*screenshot-options* (constantly opts)))
(def ^:dynamic *abort-immediately*
"Controls the behaviour of immediate test aborting in case of
[[cuic.test/is*]] failure. Setting this value to `false`
means that the test run continues even if `is*` assertion fails
(reflects the behaviour of the standard `clojure.test/is`).
```clojure
;; Run the whole test regardless of is* failures
(use-fixtures
:once
(fn [t]
(binding [*abort-immediately* false]
(t))))
```"
true)
(defn set-abort-immediately!
"Globally resets the test aborting behaviour. Useful for example REPL
usage. See [[cuic.test/*abort-immediately*]] for more details."
[abort?]
{:pre [(boolean? abort?)]}
(alter-var-root #'*abort-immediately* (constantly abort?)))
(defmacro deftest*
"Cuic's counterpart for `clojure.test/deftest`. Works identically to `deftest`
but stops gracefully if test gets aborted due to an assertion failure
in [[cuic.test/is*]]. Also captures a screenshot if test throws an
unexpected error.
See namespace documentation for the complete usage example."
[name & body]
`(deftest ~name
(let [test-ns-s# ~(str *ns*)
test-name-s# ~(str name)
full-name# (str test-ns-s# "$$" test-name-s# "$$")]
(try
(binding [*current-cuic-test* {:ns test-ns-s# :name test-name-s#}]
~@body)
(catch AbortTestError e#
nil)
(catch Throwable e#
(-try-take-screenshot full-name# nil)
(throw e#))))))
(defmacro is*
"Basically a shortcut for `(is (c/wait <cond>))` but with some
improvements to error reporting. See namespace documentation
for the complete usage example.
If the tested expression does not yield truthy value within the
current [[cuic.core/*timeout*]], a assertion failure will be
reported using the standard `clojure.test` reporting mechanism.
However, the difference between `(is (c/wait <cond>))` and
`(is* <cond>)` is that former reports the failure from `(c/wait <cond>)`
whereas the latter reports the failure from `<cond>` which will
produce much better error messages and diffs when using custom test
reporters such as [eftest](https://github.com/weavejester/eftest)
or Cursive's test runner.
Because of the nature of UI tests, first assertion failure will
usually indicate the failure of the remaining assertions as well.
If each of these assertions wait the timeout before giving up,
the test run might prolong quite a bit. That's why `is*` aborts
the current test run immediately after first failed assertion. This
behaviour can be changed by setting [[cuic.test/*abort-immediately*]]
to `false` in the surrounding scope with `binding` or globally
with [[cuic.test/set-abort-immediately!]].
**For experts only:** if you are writing a custom test reporter
and want to hook into cuic internals, `is*` provides the following
report types:
```clojure
;; (is* expr) timeout occurred
{:type :cuic/timeout
:message <string> ; error message of the timeout exception
:expr <any> ; sexpr of the failed form
}
;; Screenshot was time form the current browser
{:type :cuic/screenshot-taken
:filename <string> ; absolute path to the saved screenshot file
}
;; Screenshot was failed for some reason
{:type :cuic/screenshot-failed
:cause <throwable> ; reason for failure
}
;; Test run was aborted
{:type :cuic/test-aborted
:test <symbol> ; aborted test fully qualified name
:form <any> ; sexpr of the form causing the abort
}
```"
[form]
`(let [tname# (when *current-cuic-test*
(str (:ns *current-cuic-test*) "$$" (:name *current-cuic-test*) "$$"))
res# (is (~eventually ~form))]
(when (and (:abort res#) *abort-immediately*)
(-try-take-screenshot tname# (testing-contexts-str))
(do-report {:type :cuic/test-aborted
:test (when *current-cuic-test*
(symbol (:ns *current-cuic-test*) (:name *current-cuic-test*)))
:form '~form})
(throw (AbortTestError.)))
(:value res#)))
(defn browser-test-fixture
"Convenience test fixture for UI tests. Launches a single Chrome instance
and setups it as the default browser for [[cuic.core]] functions.
Browser's headless mode is controlled either directly by the given options
or indirectly by the `cuic.headless` system property. If you're developing
tests with REPL, you probably want to set `{:jvm-opts [\"-Dcuic.headless=false\"]}`
to your REPL profile so that tests are run in REPL are using non-headless
mode but still using headless when run in CI.
This fixture covers only the basic cases. For more complex test setups,
you probably want to write your own test fixture.
See namespace documentation for the complete usage example."
([] (browser-test-fixture {}))
([{:keys [headless options]
:or {headless (not= "false" (System/getProperty "cuic.headless"))
options {}}}]
{:pre [(boolean? headless)
(map? options)]}
(fn browser-test-fixture* [t]
(with-open [chrome (chrome/launch (assoc options :headless headless))]
(binding [c/*browser* chrome]
(t))))))