-
Notifications
You must be signed in to change notification settings - Fork 4
/
api.clj
995 lines (834 loc) · 37.3 KB
/
api.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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
(ns limo.api
"The core API wrapper around selenium webdrivers"
(:require [clojure.java.io :as io]
[clojure.string :as string]
[clojure.test :refer :all]
[limo.java :as java]
[clojure.tools.logging :as log]
[environ.core :refer [env]]
[cheshire.core :as json]
[clojure.set :as set])
(:import java.util.concurrent.TimeUnit
org.openqa.selenium.firefox.FirefoxDriver
org.openqa.selenium.By
org.openqa.selenium.By$ByCssSelector
org.openqa.selenium.Dimension
[org.openqa.selenium
Keys
StaleElementReferenceException
TimeoutException
NoSuchElementException]
org.openqa.selenium.OutputType
org.openqa.selenium.TakesScreenshot
org.openqa.selenium.WebDriver
org.openqa.selenium.WebElement
org.openqa.selenium.WebDriverException
org.openqa.selenium.interactions.Actions
[org.openqa.selenium.support.ui
ExpectedCondition
ExpectedConditions
WebDriverWait]
org.openqa.selenium.support.ui.Select))
(def ^:dynamic *driver*
"The implied selenium WebDriver instance to use when invoking functions with api calls.
All API functions can explicitly accept the WebDriver as the first argument.
Otherwise if that argument is excluded, then this dynamically bounded var is
used instead.
Example:
> (limo.api/click \"#button\")
;; becomes
> (limo.api/click *driver* \"#button\")
Defaults to `nil`. Use [[set-driver!]] as a way to quickly set this variable.
"
nil)
(def ^:dynamic *default-timeout*
"The default timeout in milliseconds until limo gives up attempting to try an action.
Defaults to 15 seconds (15000).
This value is used for wait-* set of functions which most other function calls
rely upon.
The default value is generous enough to try and cover a variety of machine
speeds, but you may find value in tweaking this parameter when checking for a
negative state (eg - verifying that a checkbox isn't checked).
"
15000) ;; msec
(def ^:dynamic *default-interval*
0)
;; Internal to wait-for to prevent nesting poll loops, which creates flakier builds.
(def ^:private ^:dynamic *is-waiting* false)
(def ^:private ^:dynamic *ignore-nested-wait-exception* false)
(defn set-driver!
"Sets the current implied active selenium WebDriver ([[*driver*]]).
Note: (set-driver! nil) is a no-op.
"
[d]
(when d
(alter-var-root #'*driver* (constantly d))))
;; Helpers
(defmacro narrate [msg & args]
`(when-let [m# ~msg]
(log/info m# ~@args)))
(defn- wrap-narration [f msg]
(fn [& args]
(narrate msg (map pr-str args))
(apply f args)))
(defn- lower-case [s]
(string/lower-case (str s)))
(defn- case-insensitive= [s1 s2]
(= (lower-case s1) (lower-case s2)))
(defn- join-paths [& paths]
(apply str (interpose "/" paths)))
;; Elements
(defn element?
"Helper function. A predicate that indicates if the given value is a selenium WebElement instance."
[e]
(instance? WebElement e))
(defn ^By by
"Creates a Selenium By instance (aka - a selenium element search query) to
find an HTML element.
In general, you shouldn't probably be calling this function directly. All limo
functions call `element` which internally calls this function to find an
element.
This function accepts either a String, Selenium WebElement or a map containing
one of the following keys to indicate how to find a DOM element:
- :css or :css-selector => search by CSS selector
- :id => search by element ID attribute
- :xpath => search by element xpath
- :tag-name => search by element tag name (eg - p, h1, div)
- :link-text => search by anchor text
- :partial-link-text => search by anchor text containing some text
- :name => search by name attribute (eg - input fields)
- :class-name => search by a css class name the element has
Examples:
;; return By instance to query by CSS
> (by \"h1\")
;; Implies
> (by {:css \"h1\"})
"
[s]
(cond
(element? s) (By/id (.getId s))
(:xpath s) (By/xpath (:xpath s))
(:id s) (By/id (:id s))
(:tag-name s) (By/tagName (:tag-name s))
(:link-text s) (By/linkText (:link-text s))
(:partial-link-text s) (By/partialLinkText (:partial-link-text s))
(:name s) (By/name (:name s))
(:class-name s) (By/className (:class-name s))
(:css s) (By$ByCssSelector. (:css s))
(:css-selector s) (By/cssSelector (:css-selector s))
:else (By/cssSelector s)))
(defn ^WebElement element
"Returns a selenium WebElement of a selector that [[by]] accepts.
If selector-or-element is an [[element?]], then the value is simply returned.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([selector-or-element] (element *driver* selector-or-element))
([driver selector-or-element]
(if (element? selector-or-element)
selector-or-element
(.findElement driver (by selector-or-element)))))
(defn elements
"Returns a sequence of WebElement instances that match the selector that
[[by]] accepts.
If selector-or-element is an [[element?]], then the value is simply returned.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([selector-or-elements] (elements *driver* selector-or-elements))
([driver selector-or-elements]
(cond
(element? selector-or-elements) [selector-or-elements]
(seq? selector-or-elements) selector-or-elements
:else (.findElements driver (by selector-or-elements)))))
(defn exists?
"Returns a boolean indicates if the given selector (that [[by]] accepts)
matches an element that is not the current page the selenium browser is
viewing.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([selector-or-element] (exists? *driver* selector-or-element))
([driver selector-or-element]
(try
(not (= nil (element driver selector-or-element)))
(catch org.openqa.selenium.NoSuchElementException _ false))))
;; Polling / Waiting
(defn ^:private wait-until* ;; TODO(jeff): promote to replace wait-until fn
([pred] (wait-until* pred {}))
([pred options]
(let [{:keys [driver timeout interval poll? suppress-non-poll-exception?]
:or {driver *driver*
timeout *default-timeout*
interval *default-interval*
poll? *is-waiting*
suppress-non-poll-exception? *ignore-nested-wait-exception*}}
options]
(if poll?
(if suppress-non-poll-exception?
(pred)
(or (pred) (throw (StaleElementReferenceException. "Inside another wait-until. Forcing retry."))))
(binding [*is-waiting* true]
(let [return-value (atom nil)
wait (doto (WebDriverWait. driver (/ timeout 1000) interval)
(.ignoring StaleElementReferenceException))]
(.until wait (proxy [ExpectedCondition] []
(apply [d] (reset! return-value (pred)))))
@return-value))))))
(defn wait-until
"Runs a given predicate pred repeatedly until a timeout occurs or pred returns
a truthy value.
This is usually called by other limo APIs unless IMMEDIATE is indicated in the
docs.
Parameters:
- `pred` can return a value, and that becomes the return value of wait-until.
- `timeout` in the time in milliseconds to wait for. Uses
[[*default-timeout*]] if not explicitly specified.
- `interval` is the time in milliseconds between calling `pred`. Defaults to
0 because [[implicit-wait]] is probably what you want to set.
- `driver` is an alternative WebDriver instance to use other than [[*driver*]].
Waits & polls:
There are 3-values that dicate polling in Limo: timeout interval, sleep
interval, and browser wait interval.
- Timeout interval is the maximum time in Selenium (& Limo) which an action
can take in its entirety.
- Sleep interval is the Thead.sleep call Selenium calls inbetween calls to
`pred` returning a falsy value.
- Browser wait interval (aka - ImplicitlyWait) is the maximum time the browser
will wait for an element that matches to appear.
In our experience, the browser wait interval is usually what keeps actions
held. An action like \"click this button\" relies on the button existing
before it can be clicked.
StaleElementReferenceException are captured and indicate a falsy return
value of `pred`. Other exceptions will be rethrown.
"
([pred] (wait-until* pred))
([pred timeout] (wait-until* pred {:timeout timeout}))
([pred timeout interval] (wait-until* pred {:timeout timeout
:interval interval}))
([driver pred timeout interval] (wait-until* pred {:driver driver
:timeout timeout
:interval interval})))
(defn wait-until-clickable
"A specialized version of wait-until that waits until an element is clickable."
([selector] (wait-until-clickable *driver* selector *default-timeout*))
([driver selector timeout]
(let [wait (doto (WebDriverWait. driver (/ timeout 1000) 0)
(.ignoring StaleElementReferenceException))]
(.until wait (ExpectedConditions/elementToBeClickable (by selector))))))
(defmacro wait-for
"A specialized version of wait-until that includes narration (printing to stdout) the action that is taken."
[driver narration & body]
(if (empty? narration)
`(wait-until ~driver (fn [] ~@body) *default-timeout* *default-interval*)
`(do
(log/info (str ~@narration))
(wait-until ~driver (fn [] ~@body) *default-timeout* *default-interval*))))
(defmacro wait-for-else
"Like wait-for, but has a default return value if the waiting predicate fails."
[driver narration default-value & body]
`(try
~(if (empty? narration)
`(wait-until ~driver (fn [] ~@body) *default-timeout* *default-interval*)
`(do
(narrate ~@narration)
(wait-until ~driver (fn [] ~@body) *default-timeout* *default-interval*)))
(catch TimeoutException te#
~default-value)))
(defn implicit-wait
"Sets the driver's implicit wait interval. The implicit wait interval is poll
interval is how much a browser will wait.
There are two kinds of waits:
- The test suite polls & waits (see [[*default-interval*]],
[[*default-timeout*]], [[wait-until]], [[wait-for]], [[wait-until-clickable]],
[[wait-for-else]]). Here the test suite / limo is responsible for polling
and asking the browser to see if an element exists.
- The browser waits for an element to appear. This is what [[implicit-wait]] configures.
For example, if we ask the browser to click on #button, but it isn't
immediately available, the browser will use the implicit-wait value to
internally wait up to the given time until returning an element not found
error.
Read Selenium's explaination of waits for another perspective of the same thing:
http://www.seleniumhq.org/docs/04_webdriver_advanced.jsp#explicit-and-implicit-waits
"
([timeout] (implicit-wait *driver* timeout))
([driver timeout] (.. driver manage timeouts (implicitlyWait timeout TimeUnit/MILLISECONDS))))
;; Act on Driver
(defn execute-script
"Evaluates a given javascript string on the page.
Generally you want to control most of your interacts via the supported browser
operations, but sometimes it's needed to run some javascript on the page
directly - like activating a verbose logging mode, or forcing one into a
specific A/B test.
Parameters:
- `driver` is the selenium driver to use
- `js` is the javascript string to eval on the page.
- `js-args` is the variadic arguments of values to pass into the eval
function. These values are limited to what can be translated to javascript,
which are the following:
- numbers
- booleans
- strings
- WebElements (objects returned via [[element]])
- lists of any above types
Note:
The javascript script string is executed in anonymous closure like this:
(function(){ $JS })($JS-ARGS);
which means you'll need to use `arguments` in `js` to access arguments pass
through from clojure to javascript.
Returns:
The return value is whatever the return value of the javascript eval
expression, which are also constrained to the same types that can be
translated as a js-args value.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
[driver js & js-args]
(.executeScript driver (str js) (into-array Object js-args)))
(defn quit
"Closes the driver, which implies closing all browser windows the driver has created.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([] (quit *driver*))
([driver] (.quit driver)))
(defn delete-all-cookies
"Deletes all cookies associated with the page the browser is currently on.
There is no way in Selenium's APIs to clear all cookies in a driver without
re-creating the driver. If you wish to reuse a driver, you must navigate to
every domain and call this function to clear cookies.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([] (delete-all-cookies *driver*))
([driver] (.. driver manage deleteAllCookies)))
(defn switch-to-frame
"Changes the driver's DOM queries to target a given frame or iframe.
Drivers do no walk through child frames/iframes' DOM elements. This function
allows all subsequent calls (eg - [[element]]) will target elements inside
that frame.
See [[switch-to-main-page]] to restore querying against the page elements.
See [[switch-to-window]] to query against windows.
"
([frame-element] (switch-to-frame *driver* frame-element))
([driver frame-element]
(exists? driver frame-element {:wait? true})
(.. driver (switchTo) (frame (element driver frame-element)))))
(defn switch-to-main-page
"Changes the driver's DOM queries to target the main page body.
Drivers do no walk through child frames/iframes' DOM elements. This function
allows all subsequent calls (eg - [[element]]) will target elements directly
on the page.
See [[switch-to-frame]] to query against iframes / frames.
See [[switch-to-window]] to query against windows.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([] (switch-to-main-page *driver*))
([driver] (.. driver switchTo defaultContent)))
(defn all-windows
"Returns a sequence of all window ids (strings) that refer to specific browser
windows the driver controls.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([] (all-windows *driver*))
([driver] (seq (.getWindowHandles driver))))
(defn switch-to-window
"Changes the driver's DOM queries to target a given browser window.
See [[switch-to-frame]] to query against iframes / frames.
See [[all-windows]] to list all window ids
See [[active-window]] to get the current window id
"
([window-handle] (switch-to-window *driver* window-handle))
([driver window-handle]
(wait-for driver [(format "switch-to-window %s" (pr-str window-handle))]
(some (partial = window-handle) (all-windows driver)))
(.. driver (switchTo) (window window-handle))))
(defn active-window
"Returns the window id (string) of the current window that the driver is focused on.
The active-window is the window where (switch-to-window (active-window)) is a no-op.
See [[switch-to-window]] to switch focused window.
See [[all-windows]] to list all window ids
See [[active-window]] to get the current window id
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([] (active-window *driver*))
([driver] (.getWindowHandle driver)))
(defmacro in-new-window
"Creates a temporary new browser window that `do-body` runs inside."
([opts action do-body] `(in-new-window *driver* ~opts ~action ~do-body))
([driver {:keys [auto-close?]} action do-body]
`(let [prev-handle# (active-window ~driver)
old-handles# (all-windows ~driver)]
~action
(wait-until #(> (count (all-windows ~driver))
(count old-handles#)))
(switch-to-window ~driver
(first (set/difference (set (all-windows ~driver))
old-handles#)))
~do-body
(if ~auto-close?
(wait-until #(= (count (set (all-windows ~driver)))
(count old-handles#)))
(.close ~driver))
(switch-to-window ~driver prev-handle#))))
(defn read-logs!
"Retrieves logs of a given type from the browser being control by selenium.
NOTE: The browser may discard the log information after the request to retrive
the logs occurs. This means multiple calls to readonly-logs! can return different
results.
> (count (read-logs!)) => 5
> (count (read-logs!)) => 0
read-logs! is pretty low-level in comparison to most of the other limo apis.
Considering using [[read-performance-logs-until-test-pass!]]
"
([log-type-kw] (read-logs! *driver* log-type-kw))
([driver log-type-kw]
(->> (.. driver
manage
logs
(get (java/->log-type log-type-kw)))
seq
(map java/log-entry->map))))
(defn read-json-logs!
"Identical read-logs!, but parses the message body as JSON.
NOTE: the same limitations as read-logs! applies: that is, that the browser
may discard the log information after the request to retrive the logs occurs.
This is known to be useful with Chrome's performance logs to get network and
rendering information. Chrome's performance log data is encoded in JSON.
read-json-logs! is pretty low-level in comparison to most of the other limo apis.
Considering using [[read-performance-logs-until-test-pass!]]
"
([log-type-kw] (read-json-logs! *driver* log-type-kw))
([driver log-type-kw]
(->> (read-logs! driver log-type-kw)
(map (fn [m] (update m :message #(json/parse-string % true)))))))
;; Act on Element
(defn ^:private js-resolve [selector-or-element]
(if (string? selector-or-element)
"document.querySelector(arguments[0])"
"arguments[0]"))
(defn ^:private on-screen? [driver selector-or-element]
;; An element, relative to window
;; -----------------------------
;; |<pos.top |
;; | |
;; ++++++++++++++|++++
;; + | +
;; + | +
;; + pos.bottom>| +
;; + | +
;; + | +
;; + | +
;; + | +
;; +++++++++++++++++++
;;
(try
(.booleanValue
(execute-script driver (str "var pos = " (js-resolve selector-or-element)
".getBoundingClientRect();"
"return (0 <= pos.top && pos.top <= window.innerHeight) || (0 <= pos.bottom && pos.bottom <= window.innerHeight);")
(if (string? selector-or-element)
selector-or-element
(element selector-or-element))))
(catch WebDriverException e
(.printStackTrace e)
;; This occurs if the javascript fails to resolve an element, in which it throws:
;; org.openqa.selenium.WebDriverException: unknown error: Cannot read property 'scrollIntoView' of null
false)))
(defn scroll-to
"Scrolls the browser to a given element so that it visible on the screen.
WARNING:
Does not safely verify its inputs (since it passes along as javascript to the
browser)."
([selector-or-element] (scroll-to *driver* selector-or-element nil))
([driver selector-or-element] (scroll-to driver selector-or-element nil))
([driver selector-or-element {:keys [behavior block inline]
:or {behavior "auto"
block "center"
inline "center"}}]
(let [behavior (str behavior)
block (str block)
inline (str inline)]
(wait-until*
#(and (exists? driver selector-or-element)
(or
(on-screen? driver selector-or-element)
(try
(execute-script driver (str (js-resolve selector-or-element) ".scrollIntoView({behavior: arguments[1], block: arguments[2], inline: arguments[3]}); ")
(if (string? selector-or-element)
selector-or-element
(element selector-or-element))
behavior
block
inline)
true
(catch WebDriverException e
;; This occurs if the javascript fails to resolve an element, in which it throws:
;; org.openqa.selenium.WebDriverException: unknown error: Cannot read property 'scrollIntoView' of null
false))))
{:driver driver}))
selector-or-element))
(defn click
"Clicks on a given element."
([selector-or-element] (click *driver* selector-or-element))
([driver selector-or-element]
(scroll-to driver selector-or-element)
(wait-until-clickable driver selector-or-element *default-timeout*)
(wait-for driver ["click" selector-or-element]
(.click (element driver selector-or-element))
true)))
(def submit
"Alias to [[click]]. Typically reads nice when referring to submit buttons."
click)
(def toggle
"Alias to [[click]]. Typically reads nice when referring to checkboxes"
click)
(defn select-by-text
"Selects a given option in a drop-down element by the user-visible text on the element.
Useful if you know you want to select a given option that is visible on screen
and its value changes more often that its display text.
"
([selector-or-element value] (select-by-text *driver* selector-or-element value))
([driver selector-or-element value]
(scroll-to driver selector-or-element)
(wait-for driver ["select-by-text" selector-or-element value]
(doto (Select. (element driver selector-or-element))
(.selectByVisibleText value))
selector-or-element)))
(defn select-by-value
"Selects a given option in a drop-down element by the server-provided value of the element.
Useful if you know you want to select a given option that has a constant
value, but may change its user-visible text more often.
"
([selector-or-element value] (select-by-value *driver* selector-or-element value))
([driver selector-or-element value]
(scroll-to driver selector-or-element)
(wait-for driver ["select-by-value" selector-or-element value]
(doto (Select. (element driver selector-or-element))
(.selectByValue value))
selector-or-element)))
(defn send-keys
"Sends keypresses to a given element. Types on a given input field.
Characters can be strings or vector of strings.
"
([selector-or-element s] (send-keys *driver* selector-or-element s))
([driver selector-or-element s]
;; Newer selenium versions no longer allow empty CharSequences for sendKeys
(when (not-empty s)
(wait-for driver nil
(.sendKeys (element driver selector-or-element)
(into-array CharSequence (if (vector? s) s [s])))
true))))
(def input-text
"Alias to [[send-keys]]. Sends keypresses to a given element. Types on a given input field.
Characters can be strings or vector of strings.
"
send-keys)
;; Query Element
(defn tag
"Returns an element's html tag name."
([selector-or-element] (tag *driver* selector-or-element))
([driver selector-or-element]
(wait-for-else driver ["tag" selector-or-element] nil
(.getTagName (element driver selector-or-element)))))
(defn text
"Returns an element's innerText."
([selector-or-element] (text *driver* selector-or-element))
([driver selector-or-element]
(wait-for-else driver ["text" selector-or-element] ""
(.getText (element driver selector-or-element)))))
(defn attribute
"Returns an element's attribute value for a given attribute name."
([selector-or-element attr] (attribute *driver* selector-or-element attr))
([driver selector-or-element attr]
(if (= attr :text)
(text selector-or-element)
(let [attr (name attr)
boolean-attrs ["async", "autofocus", "autoplay", "checked", "compact", "complete",
"controls", "declare", "defaultchecked", "defaultselected", "defer",
"disabled", "draggable", "ended", "formnovalidate", "hidden",
"indeterminate", "iscontenteditable", "ismap", "itemscope", "loop",
"multiple", "muted", "nohref", "noresize", "noshade", "novalidate",
"nowrap", "open", "paused", "pubdate", "readonly", "required",
"reversed", "scoped", "seamless", "seeking", "selected", "spellcheck",
"truespeed", "willvalidate"]
webdriver-result (wait-for-else driver ["read-attribute" attr] nil
(.getAttribute (element driver selector-or-element) (name attr)))]
(if (some #{attr} boolean-attrs)
(when (= webdriver-result "true")
attr)
webdriver-result)))))
(defn allow-backspace?
"Returns a true if the given element can handle backspace keypresses.
Hitting backspace on anything except selects, radios, checkboxes will cause
the browser to go back to the previous page.
"
[e]
(when e
(case (tag e)
"select" false
"input" (-> (attribute e "type")
#{"radio" "checkbox" "button"}
not)
true)))
(defn has-class
"Returns a true if a given element has a class on it."
[q class]
(-> (element q)
(.getAttribute "Class")
(or "")
(.contains class)))
(defn window-size
"Returns the current window's size
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([] (window-size *driver*))
([driver]
(wait-for driver ["window-size"]
(let [d (.. driver manage window getSize)]
{:width (.getWidth d)
:height (.getHeight d)}))))
(defn window-resize
"Resizes the current window to the given dimensions.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([dimensions-map] (window-resize *driver* dimensions-map))
([driver {:keys [width height] :as dimensions-map}]
(narrate "window-resize")
(-> driver
.manage
.window
(.setSize (Dimension. width height)))))
(defn refresh
"Refreshes/Reloads the current page the browser is on.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([] (refresh *driver*))
([driver]
(narrate "refresh")
(-> driver .navigate .refresh)))
(defn to
"Navigates to a given url. As if one types on the address bar.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([^String url] (to *driver* url))
([driver ^String url]
(narrate "to" url)
(-> driver .navigate (.to url))))
(defn current-url
"Returns the current url the browser is on.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([] (current-url *driver*))
([^WebDriver driver]
(narrate "current-url")
(.getCurrentUrl driver)))
(defn options
"Returns a sequence of all form value and visible text for a given drop-down"
([selector-or-element] (options *driver* selector-or-element))
([driver selector-or-element]
(let [select-elem (Select. (element driver selector-or-element))]
(map (fn [el]
{:value (.getAttribute el "value")
:text (.getText el)})
(.getAllSelectedOptions select-elem)))))
;; modified queries from taxi to retry if StaleElementReferenceException is thrown
;; Any timeouts (aka - element not found) are converted to default return values
(defn visible?
"Returns true if the given element is visible?"
([selector-or-element] (visible? *driver* selector-or-element))
([driver selector-or-element]
(wait-for-else driver ["visible?" selector-or-element] false
(.isDisplayed (element driver selector-or-element)))))
(defn selected?
"Returns true if the given element is selected (eg - checkbox)"
([selector-or-element] (selected? *driver* selector-or-element))
([driver selector-or-element]
(wait-for-else driver ["selected?" selector-or-element] false
(.isSelected (element driver selector-or-element)))))
(defn value
"Returns the input's value of a given input element"
([selector-or-element] (value *driver* selector-or-element))
([driver selector-or-element]
(wait-for-else driver ["value" selector-or-element] ""
(.getAttribute (element driver selector-or-element) "value"))))
(defn invisible?
"Returns true if the given element is invisible."
([selector-or-element] (invisible? *driver* selector-or-element))
([driver selector-or-element]
(wait-for-else driver ["invisible?" selector-or-element] false
(not (.isDisplayed (element driver selector-or-element))))))
(defn current-url-contains?
"Returns true if the current url contains some text"
[substr]
(narrate "current-url-contains?" (pr-str substr))
(let [result (try
(wait-until #(.contains (current-url) substr))
(catch TimeoutException te
false))]
(narrate " -> " (pr-str result))
result))
;; Assert on Elements
(defn text=
"Returns true if the element has innerText of a given value. The comparison is
case-insensitive and will timeout if a match does not occur."
([selector-or-element expected-value] (text= *driver* selector-or-element expected-value))
([driver selector-or-element expected-value]
(wait-for-else driver ["assert text=" selector-or-element expected-value] false
(case-insensitive= (.getText (element driver selector-or-element)) expected-value))))
(defn value=
"Returns true if the element has a value of a given string. The comparison is
case-insensitive and will timeout if a match does not occur."
([selector-or-element expected-value] (value= *driver* selector-or-element expected-value))
([driver selector-or-element expected-value]
(wait-for-else driver ["assert value=" selector-or-element expected-value] false
(case-insensitive= (.getAttribute (element driver selector-or-element) "value")
expected-value))))
(defn contains-text?
"Returns true if the element has innerText contains a given value. The comparison is
case-insensitive and will timeout if a match does not occur."
([selector-or-element expected-substr] (contains-text? *driver* selector-or-element expected-substr))
([driver selector-or-element expected-substr]
(wait-for-else driver ["assert contains-text?" selector-or-element expected-substr] false
(.contains (lower-case (.getText (element driver selector-or-element)))
(lower-case expected-substr)))))
(defn num-elements=
"Returns true if the element has a certain number of elements that matches the given query."
([selector-or-element expected-count] (num-elements= *driver* selector-or-element expected-count))
([driver selector-or-element expected-count]
(wait-for-else driver ["assert num-elements=" selector-or-element expected-count] false
(= (count (elements selector-or-element)) expected-count))))
(defn element-matches
"Returns true if the element has a certain number of elements that matches the given query."
([selector-or-element pred] (element-matches *driver* selector-or-element pred))
([driver selector-or-element pred]
(wait-for-else driver ["match element with pred" selector-or-element] false
(pred (element selector-or-element)))))
;; Actions based on queries on elements
(defn click-when-visible
"Clicks on a given element, but makes sure it's visible before doing so."
[selector]
(is (visible? selector))
(click selector))
(defn set-checkbox
"Sets a checkbox element to the given state (true = check, false = unchecked)"
[selector checked?]
(when-not (= (selected? selector) checked?)
(toggle selector)))
;; - Form Filling
(defn clear-fields
"Like [[fill-form]], but clears all the text contents of inputs by deleting its contents.
NOTE: currently this is very naive presses backspace and delete N times, where
N is the len of the text."
[fields] ;- {selector function-or-string-to-enter}
(doseq [[selector _] (filter (fn [[key value]] (string? value)) fields)]
(when (allow-backspace? selector)
(let [times (count (value selector))]
(send-keys selector (vec (repeat times Keys/BACK_SPACE)))
(send-keys selector (vec (repeat times Keys/DELETE)))))))
(defn normalize-fields
"Converts all string values that indicate typing text into functions"
[fields] ;- {selector function-or-string} -> {selector function}
(into {}
(map (fn [[k v]] [k (if (string? v)
#(input-text % v)
v)])
fields)))
(defn- fill-form*
([fields] ;- {selector function-or-string-to-enter}
(doseq [[selector action] (normalize-fields fields)]
(action selector)))
([fields & more-fields]
(fill-form* fields)
(apply fill-form* more-fields)))
(defn fill-form
"Fills forms either by input text (if a string is given) or calling a function.
This function is variadic to allow ordered-filling of inputs.
If text is filled in, then its prior contents is cleared first.
Example:
(fill-form {\"input[name=name]\" \"my name\"
\"input[email=email]\" \"me@example.com\"}
{\"input[type=submit]\" click})
"
([fields1 fields2 & more-fields] ;- {selector function-or-string-to-enter}
(fill-form fields1)
(apply fill-form fields2 more-fields))
([fields]
(clear-fields fields)
(fill-form* fields)
(doseq [[selector value] (filter string? fields)]
(is (value= selector value)
(format "Failed to fill form element '%s' with '%s' (mis-entered to '%s')"
selector value (value selector))))))
;; Screenshots
(defn ^OutputType take-screenshot
"Tells the driver to capture a screenshot of the currently active window.
Paramters:
format: Can be :file, :base64, or :bytes which prescribes the return value
destination: An optional path to save the screenshot to disk. Set nil to ignore.
Returns:
org.openqa.selenium.OutputType instance with the screenshot data in the desired format.
IMMEDIATE:
This function is considered immediate, and does not poll using [[wait-until]]
or [[wait-for]]. Thus, it is unaffected by [[*default-timeout*]].
"
([] (take-screenshot *driver* :file))
([format] (take-screenshot *driver* format nil))
([format destination] (take-screenshot *driver* format destination))
([driver format destination]
(let [driver ^TakesScreenshot driver
output (.getScreenshotAs driver (java/->output-type format))]
(if destination
(do
(io/copy output (io/file destination))
(log/info "Screenshot written to destination")
output)
output))))
(defn- screenshot-dir []
(or (:circle-artifacts env)
"screenshots"))
(defn- save-screenshot [name screenshot-dir]
(let [f (io/file (screenshot-dir) name)]
(io/make-parents f)
(take-screenshot :file f)))
(defn screenshot
"A higher-level function to take a screenshot and immediately save it on disk."
([name] (screenshot name screenshot-dir))
([name dir-f] (save-screenshot (str name ".png") dir-f)))
;; Window Size
(defn with-window-size*
"Use [[with-window-size]] instead."
([new-size actions-fn]
(with-window-size* *driver* new-size actions-fn))
([driver new-size actions-fn]
(let [w-size (window-size driver)]
(window-resize driver new-size)
(let [result (actions-fn)]
(window-resize driver w-size)
result))))
(defmacro with-window-size
"Temporarily resizes the current driver window when evaluating the body expression."
[new-size & body]
`(with-window-size* ~new-size (fn [] ~@body)))