├── resources
├── log4j.properties
├── admin.html
├── javascript.html
├── clojure.html
├── welcome.html
├── page.html
└── form.html
├── .travis.yml
├── .gitignore
├── src
└── webdriver
│ ├── core_wait.clj
│ ├── core_actions.clj
│ ├── core_window.clj
│ ├── firefox.clj
│ ├── form.clj
│ ├── js
│ └── browserbot.clj
│ ├── core_by.clj
│ ├── core_element.clj
│ ├── core_driver.clj
│ ├── util.clj
│ └── core.clj
├── script
├── util
├── grid-hub
├── grid-node
└── release
├── test
└── webdriver
│ ├── chrome_test.clj
│ ├── phantomjs_test.clj
│ ├── test
│ ├── example_app.clj
│ ├── helpers.clj
│ └── common.clj
│ ├── saucelabs_test.clj
│ ├── firefox_test.clj
│ ├── window_test.clj
│ └── util_test.clj
├── project.clj
└── README.md
/resources/log4j.properties:
--------------------------------------------------------------------------------
1 | log4j.logger.clj_webdriver=WARN
2 |
--------------------------------------------------------------------------------
/resources/admin.html:
--------------------------------------------------------------------------------
1 |
Admin Page
2 |
3 | You made it! The username and password were quite difficult to guess, weren't they?
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | script:
3 | - lein do clean, test :ci
4 | - lein do clean, with-profile +1.6 test :ci
5 | jdk:
6 | - oraclejdk8
7 | - oraclejdk7
8 | sudo: false
9 | branches:
10 | only:
11 | - master
12 | - 0.7.x
13 | - api-experimentation
14 | before_install:
15 | - "export DISPLAY=:99.0"
16 | - "sh -e /etc/init.d/xvfb start"
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml
2 | pom.xml.asc
3 | *jar
4 | lib
5 | classes
6 | outline*.html
7 | .lein*
8 | /results.txt
9 | *.properties
10 | !resources/log4j.properties
11 | /wiki
12 | test-output.txt
13 | chromedriver.log
14 | *.tar.gz
15 | /target/
16 | /test.log
17 | .vagrant
18 | *.log
19 |
20 | .nrepl-port
21 | test/settings.edn
22 |
23 | /api-docs
24 |
25 | # Intellij IDEA
26 | /.idea
27 | *.iml
28 |
--------------------------------------------------------------------------------
/src/webdriver/core_wait.clj:
--------------------------------------------------------------------------------
1 | (in-ns 'webdriver.core)
2 |
3 | (extend-type WebDriver
4 |
5 | IWait
6 | (implicit-wait [wd timeout]
7 | (.implicitlyWait (.. wd manage timeouts) timeout TimeUnit/MILLISECONDS)
8 | wd)
9 |
10 | (wait-until
11 | ([wd pred] (wait-until wd pred 5000 0))
12 | ([wd pred timeout] (wait-until wd pred timeout 0))
13 | ([wd pred timeout interval]
14 | (let [wait (WebDriverWait. wd (/ timeout 1000) interval)]
15 | (.until wait (proxy [ExpectedCondition] []
16 | (apply [d] (pred d))))
17 | wd))))
18 |
--------------------------------------------------------------------------------
/script/util:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | #
4 | # Sourced only
5 | #
6 |
7 | # Text color variables
8 | txtund=$(tput sgr 0 1) # Underline
9 | txtbld=$(tput bold) # Bold
10 | regred=$(tput setaf 1) # Red
11 | regblu=$(tput setaf 4) # Blue
12 | reggrn=$(tput setaf 2) # Green
13 | regwht=$(tput setaf 7) # White
14 | txtrst=$(tput sgr0) # Reset
15 | info=${regwht}*${txtrst} # Feedback
16 | pass=${regblu}*${txtrst}
17 | warn=${regred}*${txtrst}
18 | ques=${regblu}?${txtrst}
19 |
20 | FAIL_MSG="${regred}[FAILURE]${txtrst}"
21 | SUCCESS_MSG="${reggrn}[SUCCESS]${txtrst}"
22 |
--------------------------------------------------------------------------------
/resources/javascript.html:
--------------------------------------------------------------------------------
1 | JavaScript Playground
2 |
3 |
11 |
12 |
27 |
--------------------------------------------------------------------------------
/src/webdriver/core_actions.clj:
--------------------------------------------------------------------------------
1 | (in-ns 'webdriver.core)
2 |
3 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
4 | ;; Functions for Actions Class ;;
5 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
6 |
7 | ;; TODO: test coverage
8 | (defmacro ->build-composite-action
9 | "Create a composite chain of actions, then call `.build()`. This does **not** execute the actions; it simply sets up an 'action chain' which can later by executed using `.perform()`.
10 |
11 | Unless you need to wait to execute your composite actions, you should prefer `->actions` to this macro."
12 | [driver & body]
13 | `(let [acts# (doto (Actions. (.webdriver ~driver))
14 | ~@body)]
15 | (.build acts#)))
16 |
17 | ;; TODO: test coverage
18 | (defmacro ->actions
19 | [driver & body]
20 | `(let [act# (Actions. (.webdriver ~driver))]
21 | (doto act#
22 | ~@body
23 | .perform)
24 | ~driver))
25 |
26 | ;; e.g.
27 | ;; Action dragAndDrop = builder.clickAndHold(someElement)
28 | ;; .moveToElement(otherElement)
29 | ;; .release(otherElement)
30 | ;; .build()
31 |
--------------------------------------------------------------------------------
/test/webdriver/chrome_test.clj:
--------------------------------------------------------------------------------
1 | (ns ^:chrome webdriver.chrome-test
2 | (:require [clojure.test :refer :all]
3 | [clojure.tools.logging :as log]
4 | [webdriver.test.helpers :refer :all]
5 | [webdriver.core :refer [new-webdriver to quit]]
6 | [webdriver.test.common :as c])
7 | (:import org.openqa.selenium.remote.DesiredCapabilities
8 | org.openqa.selenium.chrome.ChromeDriver))
9 |
10 | ;; Driver definitions
11 | (log/debug "The Chrome driver requires a separate download. See the Selenium-WebDriver wiki for more information if Chrome fails to start.")
12 | (def chrome-driver (atom nil))
13 |
14 | ;; Fixtures
15 | (defn restart-browser
16 | [f]
17 | (when-not @chrome-driver
18 | (reset! chrome-driver
19 | (new-webdriver {:browser :chrome})))
20 | (to @chrome-driver *base-url*)
21 | (f))
22 |
23 | (defn quit-browser
24 | [f]
25 | (f)
26 | (quit @chrome-driver))
27 |
28 | (use-fixtures :once start-system! stop-system! quit-browser)
29 | (use-fixtures :each restart-browser)
30 |
31 | (c/defcommontests "test-" @chrome-driver)
32 |
--------------------------------------------------------------------------------
/test/webdriver/phantomjs_test.clj:
--------------------------------------------------------------------------------
1 | (ns webdriver.phantomjs-test
2 | (:require [clojure.tools.logging :as log]
3 | [clojure.test :refer :all]
4 | [webdriver.core :refer [new-webdriver to quit]]
5 | [webdriver.test.common :refer [defcommontests]]
6 | [webdriver.test.helpers :refer [*base-url* start-system! stop-system!]])
7 | (:import org.openqa.selenium.remote.DesiredCapabilities))
8 |
9 | (log/debug "The PhantomJS driver requires a separate download. See https://github.com/detro/ghostdriver for more information if PhantomJS fails to start.")
10 | (def phantomjs-driver (atom nil))
11 |
12 | ;; Fixtures
13 | (defn restart-browser
14 | [f]
15 | (when-not @phantomjs-driver
16 | (reset! phantomjs-driver
17 | (new-webdriver {:browser :phantomjs})))
18 | (to @phantomjs-driver *base-url*)
19 | (f))
20 |
21 | (defn quit-browser
22 | [f]
23 | (f)
24 | (quit @phantomjs-driver))
25 |
26 | (use-fixtures :once start-system! stop-system! quit-browser)
27 | (use-fixtures :each restart-browser)
28 |
29 | ;; RUN TESTS HERE
30 | (defcommontests "test-" @phantomjs-driver)
31 |
--------------------------------------------------------------------------------
/resources/clojure.html:
--------------------------------------------------------------------------------
1 | Clojure
2 |
3 | To quote the official Clojure website:
4 |
5 |
6 | Clojure is a dynamic programming language that targets the Java Virtual Machine (and the CLR). It is designed to be a general-purpose language, combining the approachability and interactive development of a scripting language with an efficient and robust infrastructure for multithreaded programming. Clojure is a compiled language - it compiles directly to JVM bytecode, yet remains completely dynamic. Every feature supported by Clojure is supported at runtime. Clojure provides easy access to the Java frameworks, with optional type hints and type inference, to ensure that calls to Java can avoid reflection.
7 |
8 |
9 | Clojure is a dialect of Lisp, and shares with Lisp the code-as-data philosophy and a powerful macro system. Clojure is predominantly a functional programming language, and features a rich set of immutable, persistent data structures. When mutable state is needed, Clojure offers a software transactional memory system and reactive Agent system that ensure clean, correct, multithreaded designs.
10 |
11 |
--------------------------------------------------------------------------------
/test/webdriver/test/example_app.clj:
--------------------------------------------------------------------------------
1 | (ns webdriver.test.example-app
2 | (:require [clojure.java.io :as io]
3 | [net.cgrand.moustache :refer [app]]
4 | [net.cgrand.enlive-html :refer [content defsnippet deftemplate any-node]]
5 | [ring.util.response :refer [response]]))
6 |
7 | (deftemplate page "page.html" [styles scripts cnt]
8 | [:div#content] (content cnt))
9 |
10 | (def any [:body :> any-node])
11 |
12 | (defsnippet welcome-page "welcome.html" any [])
13 | (defsnippet clojure-page "clojure.html" any [])
14 | (defsnippet example-form "form.html" any [])
15 | (defsnippet javascript-playground-page "javascript.html" any [])
16 | (defsnippet admin-page "admin.html" any [])
17 |
18 | (defn view-page
19 | [page-fn]
20 | (fn [_] (response (page nil nil (page-fn)))))
21 |
22 | (def view-frontpage (view-page welcome-page))
23 | (def view-clojure-page (view-page clojure-page))
24 | (def view-example-form (view-page example-form))
25 | (def view-javascript-playground (view-page javascript-playground-page))
26 | (def view-admin-page (view-page admin-page))
27 |
28 | (defn static-resource
29 | [req]
30 | (when-let [path (:uri req)]
31 | (when (.startsWith ^String path "/public")
32 | (response (slurp (io/resource (subs path 1)))))))
33 |
34 | (def routes
35 | (app
36 | [""] view-frontpage
37 | ["clojure"] view-clojure-page
38 | ["example-form"] view-example-form
39 | ["js-playground"] view-javascript-playground
40 | ["admin" &] view-admin-page
41 | [&] static-resource))
42 |
--------------------------------------------------------------------------------
/test/webdriver/saucelabs_test.clj:
--------------------------------------------------------------------------------
1 | (ns ^:saucelabs webdriver.saucelabs-test
2 | "Tests running on SauceLabs using 'Open Sauce' subscription"
3 | (:require [clojure.test :refer [deftest use-fixtures]]
4 | [webdriver.core :refer [quit to]]
5 | [webdriver.test.helpers :refer :all]
6 | [webdriver.test.common :refer [defcommontests]])
7 | (:import java.net.URL
8 | [java.util.logging Level]
9 | org.openqa.selenium.Platform
10 | [org.openqa.selenium.remote CapabilityType DesiredCapabilities RemoteWebDriver]))
11 |
12 | (def server (atom nil))
13 | (def driver (atom nil))
14 |
15 | (defn restart-session
16 | [f]
17 | (when (not @driver)
18 | (let [{:keys [user token host port]} (:saucelabs system)
19 | caps (doto (DesiredCapabilities.)
20 | (.setCapability CapabilityType/BROWSER_NAME "firefox")
21 | (.setCapability CapabilityType/PLATFORM Platform/MAC)
22 | (.setCapability "name" "clj-webdriver-test-suite"))
23 | url (str "http://" user ":" token "@" host ":" port "/wd/hub")
24 | wd (RemoteWebDriver. (URL. url) caps)
25 | session-id (str (.getSessionId wd))]
26 | (reset! driver wd)))
27 | (to @driver heroku-url)
28 | (f))
29 |
30 | (defn quit-session
31 | [f]
32 | (f)
33 | (quit @driver))
34 |
35 | (use-fixtures :once start-system! stop-system! quit-session)
36 | (use-fixtures :each restart-session)
37 |
38 | ;; RUN TESTS HERE
39 | (defcommontests "test-" heroku-url @driver)
40 |
--------------------------------------------------------------------------------
/test/webdriver/firefox_test.clj:
--------------------------------------------------------------------------------
1 | (ns webdriver.firefox-test
2 | (:require [clojure.test :refer :all]
3 | [webdriver.core :refer [new-webdriver current-url find-element find-elements quit get-screenshot attribute to with-driver]]
4 | [webdriver.test.common :as c]
5 | [clojure.java.io :as io]
6 | [clojure.tools.logging :as log]
7 | [webdriver.firefox :as ff]
8 | [webdriver.test.helpers :refer [*base-url* start-system! stop-system!]])
9 | (:import org.openqa.selenium.WebDriver))
10 |
11 | ;; Driver definitions
12 | (def firefox-driver (atom nil))
13 |
14 | ;; Fixtures
15 | (defn restart-browser
16 | [f]
17 | (when-not @firefox-driver
18 | (reset! firefox-driver
19 | (new-webdriver {:browser :firefox})))
20 | (to @firefox-driver *base-url*)
21 | (f))
22 |
23 | (defn quit-browser
24 | [f]
25 | (f)
26 | (quit @firefox-driver))
27 |
28 | (use-fixtures :once start-system! stop-system! quit-browser)
29 | (use-fixtures :each restart-browser)
30 |
31 | (c/defcommontests "test-" @firefox-driver)
32 |
33 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
34 | ;;; ;;;
35 | ;;; SPECIAL CASE FUNCTIONALITY ;;;
36 | ;;; ;;;
37 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
38 |
39 | ;; Firefox-specific Functionality
40 |
41 | (deftest firefox-should-support-custom-profiles
42 | (is (with-driver [tmp-dr (new-webdriver {:browser :firefox
43 | :profile (ff/new-profile)})]
44 | (log/info "[x] Starting Firefox with custom profile.")
45 | (instance? WebDriver tmp-dr))))
46 |
--------------------------------------------------------------------------------
/src/webdriver/core_window.clj:
--------------------------------------------------------------------------------
1 | (in-ns 'webdriver.core)
2 |
3 | (comment "Getting a Window for a WebDriver"
4 | (let [manage (.manage wd)]
5 | (.window manage)))
6 |
7 | (extend-protocol IWindow
8 | WebDriver
9 | (maximize [wd]
10 | (let [wnd (window* wd)]
11 | (.maximize wnd)
12 | wd))
13 |
14 | (position [wd]
15 | (let [wnd (window* wd)
16 | pnt (.getPosition wnd)]
17 | {:x (.getX pnt) :y (.getY pnt)}))
18 |
19 | (reposition [wd {:keys [x y]}]
20 | (let [wnd (window* wd)
21 | pnt (.getPosition wnd)
22 | x (or x (.getX pnt))
23 | y (or y (.getY pnt))]
24 | (.setPosition wnd (Point. x y))
25 | wd))
26 |
27 | (resize [wd {:keys [width height]}]
28 | (let [^WebDriver$Window wnd (window* wd)
29 | dim (.getSize wnd)
30 | width (or width (.getWidth dim))
31 | height (or height (.getHeight dim))]
32 | (.setSize wnd (Dimension. width height))
33 | wd))
34 |
35 | (window-size [wd]
36 | (let [^WebDriver$Window wnd (window* wd)
37 | dim (.getSize wnd)]
38 | {:width (.getWidth dim) :height (.getHeight dim)}))
39 |
40 | WebDriver$Window
41 | (maximize [window]
42 | (.maximize window)
43 | window)
44 |
45 | (position [window]
46 | (let [pnt (.getPosition window)]
47 | {:x (.getX pnt) :y (.getY pnt)}))
48 |
49 | (reposition [window {:keys [x y]}]
50 | (let [pnt (.getPosition window)
51 | x (or x (.getX pnt))
52 | y (or y (.getY pnt))]
53 | (.setPosition window (Point. x y))
54 | window))
55 |
56 | (resize [window {:keys [width height]}]
57 | (let [dim (.getSize window)
58 | width (or width (.getWidth dim))
59 | height (or height (.getHeight dim))]
60 | (.setSize window (Dimension. width height))
61 | window))
62 |
63 | (window-size [window]
64 | (let [dim (.getSize window)]
65 | {:width (.getWidth dim) :height (.getHeight dim)})))
66 |
--------------------------------------------------------------------------------
/src/webdriver/firefox.clj:
--------------------------------------------------------------------------------
1 | (ns webdriver.firefox
2 | (:require [clojure.java.io :as io])
3 | (:import org.openqa.selenium.firefox.FirefoxProfile))
4 |
5 | (defn new-profile
6 | "Create an instance of `FirefoxProfile`"
7 | ([] (FirefoxProfile.))
8 | ([profile-dir] (FirefoxProfile. (io/file profile-dir))))
9 |
10 | (defn enable-extension
11 | "Given a `FirefoxProfile` object, enable an extension. The `extension` argument should be something clojure.java.io/as-file will accept."
12 | [^FirefoxProfile profile extension]
13 | (.addExtension profile (io/as-file extension)))
14 |
15 | (defn set-preferences
16 | "Given a `FirefoxProfile` object and a map of preferences, set the preferences for the profile."
17 | [^FirefoxProfile profile pref-map]
18 | (doseq [[k v] pref-map
19 | :let [key (name k)]]
20 | ;; reflection warnings
21 | (cond
22 | (string? v) (.setPreference profile key ^String v)
23 | (instance? Boolean v) (.setPreference profile key ^Boolean v)
24 | (number? v) (.setPreference profile key ^int v))))
25 |
26 | (defn accept-untrusted-certs
27 | "Set whether or not Firefox should accept untrusted certificates."
28 | [^FirefoxProfile profile bool]
29 | (.setAcceptUntrustedCertificates profile bool))
30 |
31 | (defn enable-native-events
32 | "Set whether or not native events should be enabled (true by default on Windows, false on other platforms)."
33 | [^FirefoxProfile profile bool]
34 | (.setEnableNativeEvents profile bool))
35 |
36 | (defn write-to-disk
37 | "Write the given profile to disk. Makes sense when building up an anonymous profile via clj-webdriver."
38 | [^FirefoxProfile profile]
39 | (.layoutOnDisk profile))
40 |
41 | (defn json
42 | "Return JSON representation of the given `profile` (can be used to read the profile back in via `profile-from-json`"
43 | [^FirefoxProfile profile]
44 | (.toJson profile))
45 |
46 | (defn profile-from-json
47 | "Instantiate a new FirefoxProfile from a proper JSON representation."
48 | [^String json]
49 | (FirefoxProfile/fromJson json))
50 |
--------------------------------------------------------------------------------
/src/webdriver/form.clj:
--------------------------------------------------------------------------------
1 | (ns webdriver.form
2 | "Utilities for filling out HTML forms."
3 | (:use [webdriver.core :only [input-text find-elements]])
4 | (:import org.openqa.selenium.WebDriver))
5 |
6 | (defn- quick-fill*
7 | ([wd k v] (quick-fill* wd k v false))
8 | ([wd k v submit?]
9 | ;; shortcuts:
10 | ;; k as string => element's id attribute
11 | ;; v as string => text to input
12 | (let [query-map (if (string? k)
13 | {:id k}
14 | k)
15 | action (if (string? v)
16 | #(input-text % v)
17 | v)
18 | target-els (find-elements wd query-map)]
19 | (if submit?
20 | (doseq [el target-els]
21 | (action el))
22 | (apply action target-els)))))
23 |
24 | (defprotocol IFormHelper
25 | "Useful functions for dealing with HTML forms"
26 | (quick-fill
27 | [wd query-action-maps]
28 | "`wd` - WebDriver
29 | `query-action-maps` - a seq of maps of queries to actions (queries find HTML elements, actions are fn's that act on them)
30 |
31 | Note that a \"query\" that is just a String will be interpreted as the id attribute of your target element.
32 | Note that an \"action\" that is just a String will be interpreted as a call to `input-text` with that String for the target text field.
33 |
34 | Example usage:
35 | (quick-fill wd
36 | [{\"first_name\" \"Rich\"}
37 | {{:class \"foobar\"} click}])")
38 | (quick-fill-submit
39 | [wd query-action-maps]
40 | "Same as `quick-fill`, but expects that the final step in your sequence will submit the form, and therefore webdriver will not return a value (since all page WebElement objects are lost in Selenium-WebDriver's cache after a new page loads)"))
41 |
42 | (extend-type WebDriver
43 | IFormHelper
44 | (quick-fill
45 | [wd query-action-maps]
46 | (doseq [entries query-action-maps
47 | [k v] entries]
48 | (quick-fill* wd k v)))
49 |
50 | (quick-fill-submit
51 | [wd query-action-maps]
52 | (doseq [entries query-action-maps
53 | [k v] entries]
54 | (quick-fill* wd k v true))))
55 |
--------------------------------------------------------------------------------
/resources/welcome.html:
--------------------------------------------------------------------------------
1 | Skip to Navigation
2 | Welcome to Ministache!
3 | This is an extremely mini Moustache application used to test my clj-webdriver project. I've tried using other websites, but I'd rather not have an external dependency and no one site I've found sports all of the form elements needed for testing.
4 |
5 | The pages (in order of creation) are:
6 |
7 | - welcome.html
8 | - form.html
9 | - clojure.html
10 |
11 |
12 | And in tabular form:
13 |
14 |
15 |
16 | | File's Name |
17 | Purpose of "File" |
18 |
19 |
20 |
21 |
22 | | welcome.html (row 1, cell 1) |
23 | Introduction, test generic HTML elements (row 1, cell 2) |
24 |
25 |
26 | | form.html (row 2, cell 1) |
27 | Test HTML form elements (row 2, cell 2) |
28 |
29 |
30 | | clojure.html (row 3, cell 1) |
31 | Test window handling (row 3, cell 2) |
32 |
33 |
34 |
35 | By the way, Clojure is amazing! for the following reasons:
36 |
37 |
38 | - It's fun (really fun)
39 | - It's a Lisp
40 | - It's simple (thanks Lisp, see Stuart for more details)
41 |
- It's hosted on the JVM and has excellent interop
42 | - It encourages a functional style of programming
43 | - It has reference data types with easy-to-use, built-in concurrency semantics
44 | - It has a Sequence API that allows you to work with collections at a higher level of abstraction
45 | - It encourages tactical use of laziness in evaluation
46 | - There's a bunch of people smarter than you or me working on it
47 |
48 |
49 | Enjoy! See the links below.
50 |
--------------------------------------------------------------------------------
/resources/page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ministache
6 |
7 |
8 |
66 |
72 |
73 |
74 |
75 |
77 |
78 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/script/grid-hub:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | #
4 | # Usage: grid-hub {start|stop}
5 | #
6 |
7 | source $(dirname $0)/util
8 |
9 | EXPECTED_ARGS=1
10 | E_BADARGS=65
11 |
12 | DO_showUsage() {
13 | echo "Usage: $(basename $0) {start|stop}"
14 | exit $E_BADARGS
15 | }
16 |
17 | if [ $# -ne $EXPECTED_ARGS ]; then
18 | DO_showUsage
19 | fi
20 |
21 | ################################################################################
22 |
23 | WEBDRIVER_SERVER_JAR=$HOME/opt/selenium-server-standalone.jar
24 | # API default is 4444, so for testing we'll use 3333
25 | WEBDRIVER_HUB_PARAMS="-role hub -port 3333"
26 | WEBDRIVER_HUB_PIDFILE="/tmp/webdriver_hub.pid"
27 |
28 | if [ ! -f $WEBDRIVER_SERVER_JAR ]; then
29 | echo "You must place the Selenium-WebDriver standalone JAR file at ${WEBDRIVER_SERVER_JAR} before proceeding."
30 | exit 1
31 | fi
32 |
33 | case "$1" in
34 | start)
35 | echo "Starting Selenium-WebDriver Grid2 hub..."
36 | if [ -f $WEBDRIVER_HUB_PIDFILE ]; then
37 | echo "${FAIL_MSG} Selenium-WebDriver Grid2 hub already running with PID $(cat $WEBDRIVER_HUB_PIDFILE). Run 'grid-hub stop' or 'grid-hub restart'."
38 | exit 1
39 | else
40 | START_HUB_CMD="java -Djava.util.logging.config.file=test/logging.properties -jar ${WEBDRIVER_SERVER_JAR} ${WEBDRIVER_HUB_PARAMS}"
41 | $START_HUB_CMD &
42 | PID=$!
43 | echo $PID > "${WEBDRIVER_HUB_PIDFILE}"
44 | echo "${SUCCESS_MSG} Selenium-WebDriver Grid2 hub started successfully."
45 | echo "To see full log output, remove the java.util.logging.config.file parameter from script/grid-hub"
46 | fi
47 | ;;
48 | stop)
49 | echo "Stopping Selenium-WebDriver Grid2 hub..."
50 | if [ -f $WEBDRIVER_HUB_PIDFILE ]; then
51 | PID=$(cat $WEBDRIVER_HUB_PIDFILE)
52 | kill $PID
53 | rm $WEBDRIVER_HUB_PIDFILE
54 | sleep 1
55 | if [[ $(ps -A | egrep "^${PID}") ]]; then
56 | echo "${FAIL_MSG} Tried to kill the hub with PID ${PID}, but was unsuccessful. You need to kill it with something stronger, like 'kill -9'"
57 | exit 1
58 | else
59 | echo "${SUCCESS_MSG} Selenium-WebDriver Grid2 hub stopped successfully."
60 | exit 0
61 | fi
62 | else
63 | echo "${SUCCESS_MSG} Selenium-WebDriver Grid2 hub has already been stopped."
64 | exit 0
65 | fi
66 | ;;
67 | restart)
68 | $0 stop
69 | $0 start
70 | ;;
71 | *)
72 | DO_showUsage
73 | esac
74 |
--------------------------------------------------------------------------------
/script/grid-node:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | #
4 | # Usage: grid-node {start|stop}
5 | #
6 |
7 | source $(dirname $0)/util
8 |
9 | EXPECTED_ARGS=1
10 | E_BADARGS=65
11 |
12 | DO_showUsage() {
13 | echo "Usage: $(basename $0) {start|stop}"
14 | exit $E_BADARGS
15 | }
16 |
17 | if [ $# -ne $EXPECTED_ARGS ]; then
18 | DO_showUsage
19 | fi
20 |
21 | ################################################################################
22 |
23 | WEBDRIVER_SERVER_JAR=$HOME/opt/selenium-server-standalone.jar
24 | # API default is 4444, so for testing we'll use 3333
25 | WEBDRIVER_NODE_PARAMS="-role webdriver -hubHost 127.0.0.1 -hubPort 3333 -host 127.0.0.1 -browserName=firefox"
26 | WEBDRIVER_NODE_PIDFILE="/tmp/webdriver_node.pid"
27 |
28 | if [ ! -f $WEBDRIVER_SERVER_JAR ]; then
29 | echo "You must place the Selenium-WebDriver standalone JAR file at ${WEBDRIVER_SERVER_JAR} before proceeding."
30 | exit 1
31 | fi
32 |
33 | case "$1" in
34 | start)
35 | echo "Starting Selenium-WebDriver Grid2 node..."
36 | if [ -f $WEBDRIVER_NODE_PIDFILE ]; then
37 | echo "${FAIL_MSG} Selenium-WebDriver Grid2 node already running with PID $(cat $WEBDRIVER_NODE_PIDFILE). Run 'grid-node stop' or 'grid-node restart'."
38 | exit 1
39 | else
40 | START_NODE_CMD="java -Djava.util.logging.config.file=test/logging.properties -jar ${WEBDRIVER_SERVER_JAR} ${WEBDRIVER_NODE_PARAMS}"
41 | $START_NODE_CMD &
42 | PID=$!
43 | echo $PID > "${WEBDRIVER_NODE_PIDFILE}"
44 | echo "${SUCCESS_MSG} Selenium-WebDriver Grid2 node started successfully."
45 | echo "To see full log output, remove the java.util.logging.config.file parameter from script/grid-node"
46 | fi
47 | ;;
48 | stop)
49 | echo "Stopping Selenium-WebDriver Grid2 node..."
50 | if [ -f $WEBDRIVER_NODE_PIDFILE ]; then
51 | PID=$(cat $WEBDRIVER_NODE_PIDFILE)
52 | kill $PID
53 | rm $WEBDRIVER_NODE_PIDFILE
54 | sleep 1
55 | if [[ $(ps -A | egrep "^${PID}") ]]; then
56 | echo "${FAIL_MSG} Tried to kill the node with PID ${PID}, but was unsuccessful. You need to kill it with something stronger, like 'kill -9'"
57 | exit 1
58 | else
59 | echo "${SUCCESS_MSG} Selenium-WebDriver Grid2 node stopped successfully."
60 | exit 0
61 | fi
62 | else
63 | echo "${SUCCESS_MSG} Selenium-WebDriver Grid2 node has already been stopped."
64 | exit 0
65 | fi
66 | ;;
67 | restart)
68 | $0 stop
69 | $0 start
70 | ;;
71 | *)
72 | DO_showUsage
73 | esac
74 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject clj-webdriver "0.7.2-SNAPSHOT"
2 | :description "Clojure API for Selenium-WebDriver"
3 | :url "https://github.com/semperos/clj-webdriver"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :min-lein-version "2.0.0"
7 | :global-vars {*warn-on-reflection* true}
8 | :dependencies [[org.clojure/tools.logging "0.2.3"]
9 | [clj-http "2.0.0"]
10 | [cheshire "5.5.0"]
11 | [org.mortbay.jetty/jetty "6.1.25"]]
12 | :deploy-repositories [["releases" :clojars]]
13 | :jar-exclusions [#".*\.html" #"^public/"]
14 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.7.0"]
15 | [org.clojure/tools.reader "0.10.0-alpha3"]
16 | [org.slf4j/slf4j-log4j12 "1.7.5"]
17 | [com.stuartsierra/component "0.2.3"]
18 | [ring/ring-jetty-adapter "1.4.0"]
19 | [enlive "1.0.0" :exclusions [org.clojure/clojure]]
20 | [net.cgrand/moustache "1.0.0" :exclusions [org.clojure/clojure ring/ring-core]]
21 | ;; Needed by "remote" code
22 | [org.seleniumhq.selenium/selenium-server "2.47.1"]
23 | ;; Needed by core code
24 | [org.seleniumhq.selenium/selenium-java "2.47.0"]
25 | [org.seleniumhq.selenium/selenium-remote-driver "2.47.1"]
26 | [com.codeborne/phantomjsdriver "1.2.1"
27 | :exclusions [org.seleniumhq.selenium/selenium-java
28 | org.seleniumhq.selenium/selenium-server
29 | org.seleniumhq.selenium/selenium-remote-driver]]]
30 | :plugins [[codox "0.8.13"]]
31 | :aliases {"api-docs" ["doc"]}
32 | :codox {:output-dir "api-docs"
33 | :src-dir-uri "https://github.com/semperos/clj-webdriver/blob/master/"
34 | :src-linenum-anchor-prefix "L"
35 | :defaults {:doc/format :markdown}}}
36 | :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]}}
37 | :scm {:url "git@github.com:semperos/clj-webdriver.git"}
38 | :pom-addition [:developers [:developer [:name "Daniel Gregoire"]]]
39 | :test-selectors {:default (complement (some-fn :manual-setup :saucelabs))
40 | :manual-setup :manual-setup
41 | :saucelabs :saucelabs
42 | :ci (complement (some-fn :chrome :manual-setup :saucelabs))
43 | :all (fn [m] true)})
44 |
--------------------------------------------------------------------------------
/test/webdriver/test/helpers.clj:
--------------------------------------------------------------------------------
1 | (ns webdriver.test.helpers
2 | (:require [clojure.java.io :as io]
3 | [clojure.tools.logging :as log]
4 | [clojure.tools.reader.edn :as edn]
5 | [ring.adapter.jetty :refer [run-jetty]]
6 | [com.stuartsierra.component :as component]
7 | [webdriver.test.example-app :as web-app])
8 | (:import java.io.File
9 | [org.apache.log4j PropertyConfigurator]))
10 |
11 | (log/info "Clojure version is" *clojure-version*)
12 |
13 | (let [prop-file (io/file "test/log4j.properties")]
14 | (when (.exists prop-file)
15 | (PropertyConfigurator/configure (.getPath prop-file))))
16 |
17 | (def ^:const test-port 5744)
18 |
19 | (def ^:dynamic *base-url* (str "http://127.0.0.1:" test-port "/"))
20 | (def heroku-url "http://vast-brushlands-4998.herokuapp.com/")
21 |
22 | ;; Utilities
23 | (defmacro thrown?
24 | "Return truthy if the exception in `klass` is thrown, otherwise return falsey (nil) (code adapted from clojure.test)"
25 | [klass & forms]
26 | `(try ~@forms
27 | false
28 | (catch ~klass e#
29 | true)))
30 |
31 | (defmacro immortal
32 | "Run `body` regardless of any error or exception. Useful for wait-until, where you expect the first N invocations to produce some kind of error, especially if you're waiting for an Element to appear."
33 | [& body]
34 | `(try
35 | ~@body
36 | (catch Throwable _#)))
37 |
38 | (defn exclusive-between
39 | "Ensure a number is between a min and a max, both exclusive"
40 | [n min max]
41 | (and (> n min)
42 | (< n max)))
43 |
44 | (defrecord WebServerComponent [port]
45 | component/Lifecycle
46 | (start [component]
47 | (let [start-server (fn [] (run-jetty #'web-app/routes {:port port, :join? false}))]
48 | (if-let [server (:server component)]
49 | (if (.isRunning server)
50 | component
51 | (assoc component :server (start-server)))
52 | (assoc component :server (start-server)))))
53 |
54 | (stop [component]
55 | (when-let [server (:server component)]
56 | (.stop server))
57 | (dissoc component :server)))
58 |
59 | (defn test-system
60 | "Return a system map that the component library can use."
61 | ([] (test-system nil))
62 | ([configuration-map]
63 | (component/map->SystemMap
64 | (merge {:web (WebServerComponent. test-port)}
65 | configuration-map))))
66 |
67 | ;; For things like Saucelabs credentials
68 | (defn test-config []
69 | (let [contents (try
70 | (slurp (io/as-file "test/settings.edn"))
71 | (catch Throwable _
72 | (log/warn "No test/settings.edn file found.")
73 | nil))]
74 | (if (seq contents)
75 | (edn/read-string contents)
76 | (log/warn "The test/settings.edn file has no contents."))))
77 |
78 | (def system (test-system (test-config)))
79 |
80 | (defn start-system! [f]
81 | (alter-var-root #'system component/start)
82 | (f))
83 |
84 | (defn stop-system! [f]
85 | (f)
86 | (alter-var-root #'system component/stop))
87 |
--------------------------------------------------------------------------------
/test/webdriver/window_test.clj:
--------------------------------------------------------------------------------
1 | (ns webdriver.window-test
2 | "This namespace exercises Window manipulation code, but does not currently sport assertions. Attempts to do so over the years have resulted in non-deterministic test results. A fresh stab will be taken."
3 | (:require [clojure.test :refer :all]
4 | [webdriver.core :refer :all]
5 | [webdriver.test.helpers :refer :all]))
6 |
7 | (def driver (atom nil))
8 |
9 | ;; Fixtures
10 | (defn restart-browser
11 | [f]
12 | (when-not @driver
13 | (reset! driver (new-webdriver {:browser :firefox})))
14 | (to @driver *base-url*)
15 | (f))
16 |
17 | (defn quit-browser
18 | [f]
19 | (f)
20 | (quit @driver))
21 |
22 | (use-fixtures :once start-system! stop-system! quit-browser)
23 | (use-fixtures :each restart-browser)
24 |
25 | (defn test-window-size
26 | [this]
27 | (let [small {:width 500 :height 400}
28 | large {:width 1024 :height 800}]
29 | (resize this small)
30 | ;; (is (= (window-size this) small))
31 | (resize this large)
32 | ;; (is (= (window-size this) large))
33 | ))
34 |
35 | (defn test-window-resize-with-one-dimension
36 | [this]
37 | (let [orig-size (window-size this)
38 | small {:height 400}
39 | large {:width 1024}]
40 | (resize this small)
41 | ;; (is (= (:width (window-size this)) (:width orig-size)))
42 | (resize this orig-size)
43 | ;; (is (= (window-size this) orig-size))
44 | (resize this large)
45 | ;; (is (= (:height (window-size this)) (:height orig-size)))
46 | ))
47 |
48 | (defn test-window-position
49 | [this]
50 | (let [origin (position this)
51 | new-position {:x 100 :y 245}]
52 | (reposition this new-position)
53 | ;; (is (= (position this) new-position))
54 | (reposition this origin)
55 | ;; (is (= (position this) origin))
56 | ))
57 |
58 | (defn test-window-reposition-with-one-coordinate
59 | [this]
60 | (let [origin (position this)
61 | position-y {:y 245}
62 | position-x {:x 100}]
63 | (reposition this position-y)
64 | ;; (is (= (:x (position this)) (:x origin)))
65 | (reposition this origin)
66 | ;; (is (= (position this) origin))
67 | (reposition this position-x)
68 | ;; (is (= (:y (position this)) (:y origin)))
69 | ))
70 |
71 | (defn test-window-maximizing
72 | [this]
73 | (let [orig-size (window-size (resize this {:width 300 :height 300}))
74 | max-size (window-size (maximize this))]
75 | ;; (is (> (:width max-size) (:width orig-size)))
76 | ;; (is (> (:height max-size) (:height orig-size)))
77 | ))
78 |
79 | (defn common-window-tests
80 | [this]
81 | (doseq [tst [test-window-size
82 | test-window-resize-with-one-dimension
83 | test-window-position
84 | test-window-reposition-with-one-coordinate
85 | test-window-maximizing]]
86 | (tst this)))
87 |
88 | (deftest run-window-tests
89 | (common-window-tests @driver)
90 | ;; TODO: better test would be to open two windows
91 | ;; and pass in the second one here.
92 | (common-window-tests (window @driver)))
93 |
--------------------------------------------------------------------------------
/src/webdriver/js/browserbot.clj:
--------------------------------------------------------------------------------
1 | ;; ## Browserbot ##
2 | ;;
3 | ;; WARNING: Any functions based on JavaScript execution
4 | ;; have no guaranteed behavior across browsers.
5 | ;;
6 | ;; This bit of JavaScript was borrowed from Watir-WebDriver, which
7 | ;; borrowed it from injectableSelenium.js within Selenium-WebDriver's
8 | ;; own codebase. The `getXpath` function was borrowed from
9 | ;; http://208.91.135.51/posts/show/3754
10 | (ns webdriver.js.browserbot)
11 |
12 | (def script
13 | "
14 | var browserbot = {
15 | createEventObject : function(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
16 | var evt = element.ownerDocument.createEventObject();
17 | evt.shiftKey = shiftKeyDown;
18 | evt.metaKey = metaKeyDown;
19 | evt.altKey = altKeyDown;
20 | evt.ctrlKey = controlKeyDown;
21 | return evt;
22 | },
23 |
24 | triggerEvent: function(element, eventType, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
25 | canBubble = (typeof(canBubble) == undefined) ? true: canBubble;
26 | if (element.fireEvent && element.ownerDocument && element.ownerDocument.createEventObject) {
27 | // IE
28 | var evt = this.createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown);
29 | element.fireEvent('on' + eventType, evt);
30 | } else {
31 | var evt = document.createEvent('HTMLEvents');
32 |
33 | try {
34 | evt.shiftKey = shiftKeyDown;
35 | evt.metaKey = metaKeyDown;
36 | evt.altKey = altKeyDown;
37 | evt.ctrlKey = controlKeyDown;
38 | } catch(e) {
39 | // Nothing sane to do
40 | }
41 |
42 | evt.initEvent(eventType, canBubble, true);
43 | return element.dispatchEvent(evt);
44 | }
45 | },
46 |
47 | getVisibleText: function() {
48 | var selection = getSelection();
49 | var range = document.createRange();
50 | range.selectNodeContents(document.documentElement);
51 | selection.addRange(range);
52 | var string = selection.toString();
53 | selection.removeAllRanges();
54 |
55 | return string;
56 | },
57 |
58 | getOuterHTML: function(element) {
59 | if (element.outerHTML) {
60 | return element.outerHTML;
61 | } else if (typeof(XMLSerializer) != undefined) {
62 | return new XMLSerializer().serializeToString(element);
63 | } else {
64 | throw \"can't get outerHTML in this browser\";
65 | }
66 | },
67 |
68 | getXPath: function(elt) {
69 | var path = \"\";
70 | for (; elt && elt.nodeType == 1; elt = elt.parentNode)
71 | {
72 | idx = browserbot.getElementIdx(elt);
73 | xname = elt.tagName.toLowerCase();
74 | if (idx > 1) xname += \"[\" + idx + \"]\";
75 | path = \"/\" + xname + path;
76 | }
77 | return path;
78 | },
79 |
80 | getElementIdx: function(elt) {
81 | var count = 1;
82 | for (var sib = elt.previousSibling; sib ; sib = sib.previousSibling)
83 | {
84 | if(sib.nodeType == 1 && sib.tagName == elt.tagName) count++
85 | }
86 | return count;
87 | }
88 |
89 | }
90 | ")
91 |
--------------------------------------------------------------------------------
/script/release:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | #
4 | # I want more control over version setting.
5 | #
6 |
7 | require 'fileutils'
8 | require 'optparse'
9 |
10 | #
11 | # Returns true if version string is a SNAPSHOT
12 | #
13 | def is_snapshot?(version)
14 | version.end_with? '-SNAPSHOT'
15 | end
16 |
17 | #
18 | # Ensure release and new dev versions in place,
19 | # and that new dev version is a SNAPSHOT
20 | #
21 | def validate_args(options)
22 | unless options[:release_version]
23 | abort "You must supply a -r/--release-version argument."
24 | end
25 | unless options[:new_version]
26 | abort "You must supply a -n/--new-version argument."
27 | end
28 | unless is_snapshot? options[:new_version]
29 | abort "New versions must end in '-SNAPSHOT'."
30 | end
31 | end
32 |
33 | #
34 | # Create a backup file when mv'ing
35 | #
36 | def safe_mv (from, to)
37 | to_bak = to + '_bak'
38 | FileUtils.mv to, to_bak
39 | FileUtils.mv from, to
40 | FileUtils.rm [to_bak]
41 | end
42 |
43 | #
44 | # Change official version in project.clj
45 | #
46 | def change_project_clj(version)
47 | file = 'project.clj'
48 | tmp_file = file + '_new'
49 | line_num = 0
50 | File.open(tmp_file, 'w') do |out_file|
51 | File.open(file, 'r').each do |line|
52 | if line_num == 0
53 | out_file.print "(defproject clj-webdriver \"#{version}\"\n"
54 | else
55 | out_file.print line
56 | end
57 | line_num += 1
58 | end
59 | end
60 | safe_mv tmp_file, file
61 | end
62 |
63 | #
64 | # Change all files that contain the version
65 | #
66 | def change_project_version(version)
67 | change_project_clj version
68 | end
69 |
70 | #
71 | # Returns false if repo is dirty (staged on index or not)
72 | #
73 | def git_committed?
74 | system('git diff --quiet') and system('git diff --cached --quiet')
75 | end
76 |
77 | #
78 | # Abort if repo has uncommitted changes
79 | #
80 | def assert_clean_repo
81 | unless git_committed?
82 | abort "Please commit all local changes before attempting to release."
83 | end
84 | end
85 |
86 | #
87 | # Commits changes that update the version
88 | #
89 | def git_commit(version)
90 | fail_msg = "Failed to commit changes after changing project version to #{version}"
91 | if is_snapshot? version
92 | unless system("git commit -am \"Start on version #{version}\"")
93 | abort fail_msg
94 | end
95 | else
96 | unless system("git commit -am \"Release version #{version}\"")
97 | abort fail_msg
98 | end
99 | end
100 | end
101 |
102 | #
103 | # Create tag for release version using 'v0.0.0' format
104 | #
105 | def git_tag(version)
106 | tag = 'v' + version
107 | unless system("git tag #{tag}")
108 | abort "Failed to create new tag #{tag}"
109 | end
110 | end
111 |
112 | #
113 | # Push changes to remote. Expects branch to be tracking.
114 | #
115 | def git_push
116 | unless system('git push')
117 | abort "Failed to push local changes to remote repository."
118 | end
119 | unless system('git push --tags')
120 | abort "Failed to push tags to remote repository."
121 | end
122 | end
123 |
124 | #
125 | # Performs `lein deploy`, which expects the project.clj to
126 | # have deploy repositories set up correctly.
127 | #
128 | def lein_deploy
129 | unless system('lein deploy')
130 | abort "Failed to deploy project with Leiningen."
131 | end
132 | end
133 |
134 | ###############
135 | # Entry-Point #
136 | ###############
137 |
138 | def main
139 | options = {}
140 | OptionParser.new do |opt|
141 | opt.on('-r', '--release-version RELEASE_VERSION', 'Version to use for releasing clj-webdriver') { |o| options[:release_version] = o }
142 | opt.on('-n', '--new-version NEW_VERSION', 'Version to use as next development version after release') { |o| options[:new_version] = o }
143 | end.parse!
144 | validate_args options
145 | release_version = options[:release_version]
146 | new_version = options[:new_version]
147 |
148 | assert_clean_repo
149 | change_project_version release_version
150 | git_commit release_version
151 | git_tag release_version
152 | lein_deploy
153 | change_project_version new_version
154 | git_commit new_version
155 | git_push
156 | end
157 |
158 | main()
159 |
--------------------------------------------------------------------------------
/resources/form.html:
--------------------------------------------------------------------------------
1 | Example Form
2 |
114 |
--------------------------------------------------------------------------------
/src/webdriver/core_by.clj:
--------------------------------------------------------------------------------
1 | ;; ## Core by-* Functions ##
2 | ;;
3 | ;; These functions are low-level equivalents for the
4 | ;; `ByFoo` classes that make up the Java API, with a few
5 | ;; notable exceptions that provide more flexible matching
6 | ;; (see the `by-attr*` functions at the bottom)
7 | (in-ns 'webdriver.core)
8 |
9 | (defn by-id
10 | "Used when finding elements. Returns `By/id` of `expr`"
11 | [expr]
12 | (By/id (name expr)))
13 |
14 | (defn by-link-text
15 | "Used when finding elements. Returns `By/linkText` of `expr`"
16 | [expr]
17 | (By/linkText expr))
18 |
19 | (defn by-partial-link-text
20 | "Used when finding elements. Returns `By/partialLinkText` of `expr`"
21 | [expr]
22 | (By/partialLinkText expr))
23 |
24 | (defn by-name
25 | "Used when finding elements. Returns `By/name` of `expr`"
26 | [expr]
27 | (By/name (name expr)))
28 |
29 | (defn by-tag
30 | "Used when finding elements. Returns `By/tagName` of `expr`"
31 | [expr]
32 | (By/tagName (name expr)))
33 |
34 | (defn by-xpath
35 | "Used when finding elements. Returns `By/xpath` of `expr`"
36 | [expr]
37 | (By/xpath expr))
38 |
39 | (defn by-css-selector
40 | "Used when finding elements. Returns `By/cssSelector` of `expr`"
41 | [expr]
42 | (By/cssSelector expr))
43 |
44 | (defn by-query
45 | "Given a map with either an `:xpath` or `:css` key, return the respective by-* function (`by-xpath` or `by-css-selector`) using the value for that key."
46 | [{:keys [xpath css] :as m}]
47 | (cond
48 | xpath (by-xpath (:xpath m))
49 | css (by-css-selector (:css m))
50 | :else (throw (IllegalArgumentException. "You must provide either an `:xpath` or `:css` entry."))))
51 |
52 | ;; TODO Review behavior of By/className (accepts only one or query?)
53 | (defn by-class-name
54 | "Used when finding elements. Returns `By/className` of `expr`"
55 | [expr]
56 | (let [expr (str expr)]
57 | (if (re-find #"\s" expr)
58 | (let [classes (string/split expr #"\s+")
59 | class-query (string/join "." classes)]
60 | (by-css-selector (str "*." class-query)))
61 | (By/className (name expr)))))
62 |
63 | ;; Inspired by the `attr=`, `attr-contains` in Christophe Grand's enlive
64 | (defn by-attr=
65 | "Use `value` of arbitrary attribute `attr` to find an element. You can optionally specify the tag.
66 | For example: `(by-attr= :id \"element-id\")`
67 | `(by-attr= :div :class \"content\")`"
68 | ([attr value] (by-attr= :* attr value)) ; default to * any tag
69 | ([tag attr value]
70 | (cond
71 | (= :class attr) (if (re-find #"\s" value)
72 | (let [classes (string/split value #"\s+")
73 | class-query (string/join "." classes)]
74 | (by-css-selector (str (name tag) class-query)))
75 | (by-class-name value))
76 | (= :id attr) (by-id value)
77 | (= :name attr) (by-name value)
78 | (= :tag attr) (by-tag value)
79 | (= :text attr) (if (= tag :a)
80 | (by-link-text value)
81 | (by-xpath (str "//"
82 | (name tag)
83 | "[text()"
84 | "=\"" value "\"]")))
85 | :else (by-css-selector (str (name tag)
86 | "[" (name attr) "='" value "']")))))
87 |
88 | (defn by-attr-contains
89 | "Match if `value` is contained in the value of `attr`. You can optionally specify the tag.
90 | For example: `(by-attr-contains :class \"navigation\")`
91 | `(by-attr-contains :ul :class \"tags\")`"
92 | ([attr value] (by-attr-contains :* attr value)) ; default to * any tag
93 | ([tag attr value]
94 | (by-css-selector (str (name tag)
95 | "[" (name attr) "*='" value "']"))))
96 |
97 | (defn by-attr-starts
98 | "Match if `value` is at the beginning of the value of `attr`. You can optionally specify the tag."
99 | ([attr value] (by-attr-starts :* attr value))
100 | ([tag attr value]
101 | (by-css-selector (str (name tag)
102 | "[" (name attr) "^='" value "']"))))
103 |
104 | (defn by-attr-ends
105 | "Match if `value` is at the end of the value of `attr`. You can optionally specify the tag."
106 | ([attr value] (by-attr-ends :* attr value))
107 | ([tag attr value]
108 | (by-css-selector (str (name tag)
109 | "[" (name attr) "$='" value "']"))))
110 |
111 | (defn by-has-attr
112 | "Match if the element has the attribute `attr`, regardless of its value. You can optionally specify the tag."
113 | ([attr] (by-has-attr :* attr))
114 | ([tag attr]
115 | (by-css-selector (str (name tag)
116 | "[" (name attr) "]"))))
117 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [UNMAINTAINED] Clojure API for Selenium-WebDriver #
2 |
3 | This is a Clojure library for driving a web browser using Selenium-WebDriver.
4 |
5 | You **must** add the Selenium-WebDriver JAR's you need explicitly in your project's dependencies. This library _does not_ ship with runtime dependencies on any version of Selenium-WebDriver to allow compatibility with Selenium-WebDriver's upstream releases.
6 |
7 | Please see the [Wiki](https://github.com/semperos/clj-webdriver/wiki) for prose documentation or generate API docs using `lein doc` inside this project.
8 |
9 | **Latest stable coordinates:**
10 |
11 | [](http://clojars.org/clj-webdriver)
12 |
13 | **clj-webdriver Resources**
14 |
15 | * [Project Wiki](https://github.com/semperos/clj-webdriver/wiki)
16 | * [Google Group](https://groups.google.com/forum/#!forum/clj-webdriver)
17 | * [Issue Queue](https://github.com/semperos/clj-webdriver/issues)
18 | * [Travis CI](https://travis-ci.org/semperos/clj-webdriver) [](https://travis-ci.org/semperos/clj-webdriver)
19 |
20 | **External Resources**
21 |
22 | * [Selenium-WebDriver API (Javadoc)](http://selenium.googlecode.com/svn/trunk/docs/api/java/index.html)
23 | * [Selenium-WebDriver Changelog](https://code.google.com/p/selenium/source/browse/java/CHANGELOG)
24 | * [CSS Selector Syntax](http://www.w3.org/TR/css3-selectors/#selectors)
25 |
26 | **Please join the Google group if you use this library.** I regularly post announcements about upcoming releases, and although I ensure all tests are passing and try to maintain good test coverage before releases, user testing is invaluable. Thank you!
27 |
28 | ## Contributing ##
29 |
30 | The `master` branch of clj-webdriver houses code intended for the next **minor-version release.** If you want to propose new features for the next release, you're welcome to fork, make a topic branch and issue a pull request against the `master` branch.
31 |
32 | If you want to fix a bug in the **current release**, please pull against the appropriate branch for the current minor version, **0.7.x**.
33 |
34 | ## Running Tests ##
35 |
36 | To run the default suite:
37 |
38 | ```
39 | lein test
40 | ```
41 |
42 | To run the test suite for an existing hub/node setup:
43 |
44 | ```
45 | ./script/grid-hub start
46 | ./script/grid-node start
47 | lein test :manual-setup
48 | ```
49 |
50 | To run the test suite for Saucelabs, first visit the [test app on Heroku](http://vast-brushlands-4998.herokuapp.com) to make sure it's "awake" and then run:
51 |
52 | ```
53 | lein test :saucelabs
54 | ```
55 |
56 | ## Release ##
57 |
58 | There's a Ruby script at `script/release`. It was written using version 2.2.2, no promises that it works with any other.
59 |
60 | ```
61 | ./script/release --release-version 8.8.8 --new-version 9.0.0-SNAPSHOT
62 | ```
63 |
64 | The `--release-version` can be `-r` and the `--new-version` can be `-n`. Further, the new version must end with `-SNAPSHOT`.
65 |
66 | ## Acknowledgements ##
67 |
68 | Credits to [mikitebeka/webdriver-clj](https://github.com/mikitebeka/webdriver-clj) for the initial code for this project and many of the low-level wrappers around the Selenium-WebDriver API.
69 |
70 | Many thanks to those who have contributed so far (in nick-alphabetical order):
71 |
72 | * [kapman](https://github.com/kapman)
73 | * [mangaohua](https://github.com/mangaohua)
74 | * [maxweber](https://github.com/maxweber) (Max Weber)
75 | * [RobLally](https://github.com/RobLally) (Rob Lally)
76 | * [smidas](https://github.com/smidas) (Nathan Smith)
77 | * [ulsa](https://github.com/ulsa) (Ulrik Sandberg)
78 | * [xeqi](https://github.com/xeqi) (Nelson Morris)
79 |
80 | See Github for an [up-to-date list of contributors](https://github.com/semperos/clj-webdriver/contributors)
81 |
82 | ## Open Source Tools ##
83 |
84 | I would like to thank the following companies for providing their tools free of charge to clj-webdriver developers as part of their contribution to the Open Source community.
85 |
86 | ### JetBrains: Intellij IDEA ###
87 |
88 | When I need to do Java, Scala, or even JRuby development, I rely on Intellij IDEA's excellent support for JVM languages. I would like to thank JetBrains for granting clj-webdriver developers a free license to Intellij IDEA Ultimate, now for two years running.
89 |
90 | Intellij IDEA: Java IDE with advanced HTML/CSS/JS editor for hardcore web-developers
91 |
92 | ### YourKit ###
93 |
94 | YourKit is kindly supporting open source projects with its full-featured Java Profiler.
95 | YourKit, LLC is the creator of innovative and intelligent tools for profiling
96 | Java and .NET applications. Take a look at YourKit's leading software products:
97 | YourKit Java Profiler and
98 | YourKit .NET Profiler.
99 |
100 | ## License ##
101 |
102 | Clj-webdriver is distributed under the [Eclipse Public License](http://opensource.org/licenses/eclipse-1.0.php), the same as Clojure.
103 |
--------------------------------------------------------------------------------
/test/webdriver/util_test.clj:
--------------------------------------------------------------------------------
1 | (ns webdriver.util-test
2 | (:require [clojure.test :refer :all]
3 | [webdriver.util :refer :all]))
4 |
5 | ;; Functions to test:
6 | ;;
7 | ;; * build-css-attrs
8 | ;; * build-xpath-attrs
9 | ;; * build-css-with-hierarchy
10 | ;; * build-xpath-with-hierarchy
11 | ;; * build-query
12 |
13 | (deftest test-contains-regex?
14 | (is (contains-regex? {:foo #"bar" :bar :boom}))
15 | (is (not (contains-regex? {:lang "clojure"})))
16 | (is (not (contains-regex? {}))))
17 |
18 | (deftest test-all-regex?
19 | (is (all-regex? {:foo #"bar" :baz #"boom"}))
20 | (is (not (all-regex? {:foo #"bar" :baz "boom"})))
21 | (is (not (all-regex? {:lang "clojure"})))
22 | (is (not (all-regex? {}))))
23 |
24 | (deftest test-filter-regex-entries
25 | ;; TODO: Research Pattern equality issue requiring only looking at keys here
26 | (is (= (keys (filter-regex-entries {:foo #"bar" :baz "boom"}))
27 | (keys {:foo #"bar"})))
28 | (is (= (filter-regex-entries {:lang "clojure"}) {}))
29 | (is (= (filter-regex-entries {}) {})))
30 |
31 | (deftest test-remove-regex-entries
32 | (is (= (remove-regex-entries {:foo #"bar" :baz "boom"})
33 | {:baz "boom"}))
34 | (is (= (remove-regex-entries {:lang "clojure"})
35 | {:lang "clojure"}))
36 | (is (= (remove-regex-entries {}) {})))
37 |
38 | (deftest test-first-n-chars
39 | (is (= (first-n-chars "foo" 2) "fo..."))
40 | (is (= (first-n-chars "foo" 10) "foo"))
41 | (is (= (first-n-chars "foo" 0) "...")))
42 |
43 | (deftest test-elim-linebreaks
44 | (is (re-matches #"\s+" (elim-breaks "\n\r\n")))
45 | (is (re-matches #"foo bar\s+" (elim-breaks "foo bar\r\n"))))
46 |
47 | (deftest test-dashes-to-camel-case
48 | (let [f dashes-to-camel-case]
49 | (is (= (f "foo-bar-baz") "fooBarBaz"))
50 | (is (= (f "foo-bar-bazY") "fooBarBazY"))))
51 |
52 | (deftest test-camel-case-to-dashes
53 | (let [f camel-case-to-dashes]
54 | (is (= (f "browserName") "browser-name"))
55 | (is (= (f "version") "version"))
56 | (is (= (f "trustAllSSLCertificates") "trust-all-sSL-certificates"))
57 | (is (= (f "wowzaSauceY") "wowza-sauceY"))))
58 |
59 | (deftest test-clojure-keys
60 | (is (= (clojure-keys {"browserName" "firefox"
61 | "version" 15
62 | "trustAllSSLCertificates" true
63 | "wowzaSauceY" false})
64 | {:browser-name "firefox"
65 | :version 15
66 | :trust-all-sSL-certificates true
67 | :wowza-sauceY false})))
68 |
69 | (deftest test-java-keys
70 | (is (= (java-keys {:browser-name "firefox"
71 | :version 15
72 | :trust-all-SSL-certificates true
73 | :wowza-sauceY false})
74 | {"browserName" "firefox"
75 | "version" 15
76 | "trustAllSSLCertificates" true
77 | "wowzaSauceY" false})))
78 |
79 | (deftest test-clojure-java-keys-complimentary
80 | (let [c-map {:browser-name "firefox"
81 | :version 15
82 | :trust-all-sSL-certificates true
83 | :wowza-sauceY false
84 | :safari.clean-session true}
85 | j-map {"browserName" "firefox"
86 | "version" 15
87 | "trustAllSSLCertificates" true
88 | "wowzaSauceY" false
89 | "safari.cleanSession" true}]
90 | (is (= (clojure-keys
91 | (java-keys c-map))
92 | c-map))
93 | (is (= (java-keys
94 | (clojure-keys j-map))
95 | j-map))))
96 |
97 | (def desired-capabilities
98 | [;; Browser selection
99 | "browserName" "version" "platform"
100 | ;; Read-only capabilities
101 | "takesScreenshot" "handlesAlerts" "cssSelectorsEnabled"
102 | ;; Read-write capabilities
103 | "javascriptEnabled" "databaseEnabled" "locationContextEnabled"
104 | "applicationCacheEnabled" "browserConnectionEnabled" "webStorageEnabled"
105 | "acceptSslCerts" "rotatable" "nativeEvents" "proxy" "unexpectedAlertBehaviour"
106 | ;; RemoteWebDriver specific
107 | "webdriver.remote.sessionid" "webdriver.remote.quietExceptions"
108 | ;; Grid-specific
109 | "path" "seleniumProtocol" "maxInstances" "environment"
110 | ;; Selenium RC (1.0) only
111 | "proxy_pac" "commandLineFlags" "executablePath" "timeoutInSeconds"
112 | "onlyProxySeleniumTraffic" "avoidProxy" "proxyEverything" "proxyRequired"
113 | "browserSideLog" "optionsSet" "singleWindow" "dontInjectRegex"
114 | "userJSInjection" "userExtensions"
115 | ;; Selenese-backed-WebDriver specific
116 | "selenium.server.url"
117 | ;; Browser-specific Capabilities
118 | ;;
119 | ;; Opera specific
120 | "opera.binary" "opera.guess_binary_path" "opera.no_restart" "opera.product"
121 | "opera.no_quit" "opera.autostart" "opera.display" "opera.idle" "opera.profile"
122 | "opera.launcher" "opera.port" "opera.host" "opera.arguments"
123 | "opera.logging.file" "opera.logging.level"
124 | ;; Chrome specific
125 | "chrome.chromedriverVersion" "chrome.binary" "chrome.switches" "chrome.extensions"
126 | ;; Firefox specific
127 | ;;
128 | ;; WebDriver
129 | "firefox_profile" "loggingPrefs" "firefox_binary"
130 | ;; RC
131 | "mode" "captureNetworkTraffic" "addCustomRequestHeaders" "trustAllSSLCertificates"
132 | "changeMaxConnections" "firefoxProfileTemplate" "profile"
133 | ;; IE specific
134 | ;;
135 | ;; WebDriver
136 | "ignoreProtectedModeSettings" "initialBrowserUrl"
137 | "useLegacyInternalServer" "elementScrollBehavior"
138 | ;; RC
139 | "mode" "killProcessesByName" "honorSystemProxy" "ensureCleanSession"
140 | ;; Safari specific
141 | ;;
142 | ;; WebDriver
143 | "safari.cleanSession"
144 | ;; RC
145 | "mode" "honorSystemProxy" "ensureCleanSession"
146 | ;; Object structures
147 | ;;
148 | ;; Proxy JSON Object
149 | "proxyType" "proxyAutoconfigUrl" "ftpProxy" "httpProxy" "sslProxy"
150 | ;; LoggingPreferences JSON Object
151 | "driver"
152 | ;; FirefoxProfile settings
153 | "webdriver_accept_untrusted_certs" "webdriver_assume_untrusted_issuer"
154 | "webdriver.log.driver" "webdriver.log.file" "webdriver_enable_native_events"
155 | "webdriver.load.strategy" "webdriver_firefox_port"])
156 |
157 | (def desired-capabilities-clj (mapv keyword desired-capabilities))
158 |
159 | (deftest test-desired-capabilities-java-clojure-java
160 | (let [caps-map (apply hash-map
161 | (interleave (distinct desired-capabilities)
162 | (repeat true)))
163 | clojurized-map (clojure-keys caps-map)]
164 | (is (= (java-keys clojurized-map) caps-map))))
165 |
166 | (deftest test-desired-capabilities-clojure-java
167 | (let [clj-caps-map (apply hash-map
168 | (interleave (distinct desired-capabilities-clj)
169 | (repeat true)))
170 | caps-map (apply hash-map
171 | (interleave (distinct desired-capabilities)
172 | (repeat true)))
173 | javaized-map (java-keys clj-caps-map)]
174 | (is (= javaized-map caps-map))))
175 |
--------------------------------------------------------------------------------
/src/webdriver/core_element.clj:
--------------------------------------------------------------------------------
1 | ;; ## Core Element-related Functions ##
2 | ;;
3 | ;; This namespace implements the following protocols:
4 | ;;
5 | ;; * IElement
6 | ;; * IFormElement
7 | ;; * ISelectElement
8 | (in-ns 'webdriver.core)
9 |
10 | (declare execute-script)
11 | (declare execute-script*)
12 | (defn- browserbot
13 | [driver fn-name & arguments]
14 | (let [script (str browserbot-js/script
15 | "return browserbot."
16 | fn-name
17 | ".apply(browserbot, arguments)")
18 | execute-js-fn (partial execute-script* driver script)]
19 | (apply execute-js-fn arguments)))
20 |
21 | (defn ^java.awt.Rectangle rectangle
22 | [webelement]
23 | (let [loc (location-on-page webelement)
24 | el-size (element-size webelement)]
25 | (java.awt.Rectangle. (:x loc)
26 | (:y loc)
27 | (:width el-size)
28 | (:height el-size))))
29 |
30 | (extend-type WebElement
31 | IElement
32 | (attribute [webelement attr]
33 | (if (= attr :text)
34 | (text webelement)
35 | (let [attr (name attr)
36 | boolean-attrs ["async", "autofocus", "autoplay", "checked", "compact", "complete",
37 | "controls", "declare", "defaultchecked", "defaultselected", "defer",
38 | "disabled", "draggable", "ended", "formnovalidate", "hidden",
39 | "indeterminate", "iscontenteditable", "ismap", "itemscope", "loop",
40 | "multiple", "muted", "nohref", "noresize", "noshade", "novalidate",
41 | "nowrap", "open", "paused", "pubdate", "readonly", "required",
42 | "reversed", "scoped", "seamless", "seeking", "selected", "spellcheck",
43 | "truespeed", "willvalidate"]
44 | webdriver-result (.getAttribute webelement (name attr))]
45 | (if (some #{attr} boolean-attrs)
46 | (when (= webdriver-result "true")
47 | attr)
48 | webdriver-result))))
49 |
50 | (click [webelement]
51 | (.click webelement))
52 |
53 | (css-value [webelement property]
54 | (.getCssValue webelement property))
55 |
56 | (displayed? [webelement]
57 | (.isDisplayed webelement))
58 |
59 | (exists? [webelement]
60 | webelement)
61 |
62 | (flash [webelement]
63 | (let [original-color (if (css-value webelement "background-color")
64 | (css-value webelement "background-color")
65 | "transparent")
66 | orig-colors (repeat original-color)
67 | change-colors (interleave (repeat "red") (repeat "blue"))]
68 | (doseq [flash-color (take 12 (interleave change-colors orig-colors))]
69 | (execute-script* (.getWrappedDriver ^WrapsDriver webelement)
70 | (str "arguments[0].style.backgroundColor = '"
71 | flash-color "'")
72 | webelement)
73 | (Thread/sleep 80)))
74 | webelement)
75 |
76 | (focus [webelement]
77 | (execute-script*
78 | (.getWrappedDriver ^WrapsDriver webelement) "return arguments[0].focus()" webelement)
79 | webelement)
80 |
81 | (html [webelement]
82 | (browserbot (.getWrappedDriver ^WrapsDriver webelement) "getOuterHTML" webelement))
83 |
84 | (location-on-page [webelement]
85 | (let [loc (.onPage (.getCoordinates ^Locatable webelement))]
86 | {:x (.x loc), :y (.y loc)}))
87 |
88 | (location-in-viewport [webelement]
89 | (let [loc (.inViewPort (.getCoordinates ^Locatable webelement))]
90 | {:x (.x loc), :y (.y loc)}))
91 |
92 | (present? [webelement]
93 | (and (exists? webelement) (visible? webelement)))
94 |
95 | (element-size [webelement]
96 | (let [size (.getSize webelement)]
97 | {:width (.width size), :height (.height size)}))
98 |
99 | (intersects? [webelement-a webelement-b]
100 | (let [rect-a (rectangle webelement-a)
101 | rect-b (rectangle webelement-b)]
102 | (.intersects rect-a rect-b)))
103 |
104 | (tag [webelement]
105 | (.getTagName webelement))
106 |
107 | (text [webelement]
108 | (.getText webelement))
109 |
110 | (value [webelement]
111 | (.getAttribute webelement "value"))
112 |
113 | (visible? [webelement]
114 | (.isDisplayed webelement))
115 |
116 | (xpath [webelement]
117 | (browserbot (.getWrappedDriver ^WrapsDriver webelement) "getXPath" webelement []))
118 |
119 | IFormElement
120 | (deselect [webelement]
121 | (if (.isSelected webelement)
122 | (toggle webelement)
123 | webelement))
124 |
125 | (enabled? [webelement]
126 | (.isEnabled webelement))
127 |
128 | (input-text [webelement s]
129 | (.sendKeys webelement (into-array CharSequence [s]))
130 | webelement)
131 |
132 | (submit [webelement]
133 | (.submit webelement))
134 |
135 | (clear [webelement]
136 | (.clear webelement)
137 | webelement)
138 |
139 | (select [webelement]
140 | (if-not (.isSelected webelement)
141 | (.click webelement)
142 | webelement))
143 |
144 | (selected? [webelement]
145 | (.isSelected webelement))
146 |
147 | (send-keys [webelement s]
148 | (.sendKeys webelement (into-array CharSequence [s]))
149 | webelement)
150 |
151 | (toggle [webelement]
152 | (.click webelement)
153 | webelement)
154 |
155 | ISelectElement
156 | (all-options [webelement]
157 | (let [select-list (Select. webelement)]
158 | (.getOptions select-list)))
159 |
160 | (all-selected-options [webelement]
161 | (let [select-list (Select. webelement)]
162 | (.getAllSelectedOptions select-list)))
163 |
164 | (deselect-option [webelement attr-val]
165 | {:pre [(or (= (first (keys attr-val)) :index)
166 | (= (first (keys attr-val)) :value)
167 | (= (first (keys attr-val)) :text))]}
168 | (case (first (keys attr-val))
169 | :index (deselect-by-index webelement (:index attr-val))
170 | :value (deselect-by-value webelement (:value attr-val))
171 | :text (deselect-by-text webelement (:text attr-val))))
172 |
173 | (deselect-all [webelement]
174 | (let [cnt-range (->> (all-options webelement)
175 | count
176 | (range 0))]
177 | (doseq [idx cnt-range]
178 | (deselect-by-index webelement idx))
179 | webelement))
180 |
181 | (deselect-by-index [webelement idx]
182 | (let [select-list (Select. webelement)]
183 | (.deselectByIndex select-list idx)
184 | webelement))
185 |
186 | (deselect-by-text [webelement text]
187 | (let [select-list (Select. webelement)]
188 | (.deselectByVisibleText select-list text)
189 | webelement))
190 |
191 | (deselect-by-value [webelement value]
192 | (let [select-list (Select. webelement)]
193 | (.deselectByValue select-list value)
194 | webelement))
195 |
196 | (first-selected-option [webelement]
197 | (let [select-list (Select. webelement)]
198 | (.getFirstSelectedOption select-list)))
199 |
200 | (multiple? [webelement]
201 | (let [value (attribute webelement "multiple")]
202 | (or (= value "true")
203 | (= value "multiple"))))
204 |
205 | (select-option [webelement attr-val]
206 | {:pre [(or (= (first (keys attr-val)) :index)
207 | (= (first (keys attr-val)) :value)
208 | (= (first (keys attr-val)) :text))]}
209 | (case (first (keys attr-val))
210 | :index (select-by-index webelement (:index attr-val))
211 | :value (select-by-value webelement (:value attr-val))
212 | :text (select-by-text webelement (:text attr-val))))
213 |
214 | (select-all [webelement]
215 | (let [cnt-range (->> (all-options webelement)
216 | count
217 | (range 0))]
218 | (doseq [idx cnt-range]
219 | (select-by-index webelement idx))
220 | webelement))
221 |
222 | (select-by-index [webelement idx]
223 | (let [select-list (Select. webelement)]
224 | (.selectByIndex select-list idx)
225 | webelement))
226 |
227 | (select-by-text [webelement text]
228 | (let [select-list (Select. webelement)]
229 | (.selectByVisibleText select-list text)
230 | webelement))
231 |
232 | (select-by-value [webelement value]
233 | (let [select-list (Select. webelement)]
234 | (.selectByValue select-list value)
235 | webelement))
236 |
237 | IFind
238 | (find-element-by [webelement by]
239 | (let [by (if (map? by)
240 | (by-query (build-query by :local))
241 | by)]
242 | (.findElement webelement by)))
243 |
244 | (find-elements-by [webelement by]
245 | (let [by (if (map? by)
246 | (by-query (build-query by :local))
247 | by)]
248 | (.findElements webelement by)))
249 |
250 | (find-element [webelement by]
251 | (find-element-by webelement by))
252 |
253 | (find-elements [webelement by]
254 | (find-elements-by webelement by)))
255 |
256 | ;;
257 | ;; Extend Element-related protocols to `nil`,
258 | ;; so our nil-handling is clear.
259 | ;;
260 |
261 | (extend-protocol IElement
262 | nil
263 | (attribute [n attr] (throw-nse))
264 |
265 | (click [n] (throw-nse))
266 |
267 | (css-value [n property] (throw-nse))
268 |
269 | (displayed? [n] (throw-nse))
270 |
271 | (exists? [n] false)
272 |
273 | (flash [n] (throw-nse))
274 |
275 | (focus [n] (throw-nse))
276 |
277 | (html [n] (throw-nse))
278 |
279 | (location-on-page [n] (throw-nse))
280 |
281 | (location-in-viewport [n] (throw-nse))
282 |
283 | (present? [n] (throw-nse))
284 |
285 | (element-size [n] (throw-nse))
286 |
287 | (rectangle [n] (throw-nse))
288 |
289 | (intersects? [n m-b] (throw-nse))
290 |
291 | (tag [n] (throw-nse))
292 |
293 | (text [n] (throw-nse))
294 |
295 | (value [n] (throw-nse))
296 |
297 | (visible? [n] (throw-nse))
298 |
299 | (xpath [n] (throw-nse)))
300 |
301 | (extend-protocol IFormElement
302 | nil
303 | (deselect [n] (throw-nse))
304 |
305 | (enabled? [n] (throw-nse))
306 |
307 | (input-text [n s] (throw-nse))
308 |
309 | (submit [n] (throw-nse))
310 |
311 | (clear [n] (throw-nse))
312 |
313 | (select [n] (throw-nse))
314 |
315 | (selected? [n] (throw-nse))
316 |
317 | (send-keys [n s] (throw-nse))
318 |
319 | (toggle [n] (throw-nse)))
320 |
321 | (extend-protocol ISelectElement
322 | nil
323 | (all-options [n] (throw-nse))
324 |
325 | (all-selected-options [n] (throw-nse))
326 |
327 | (deselect-option [n attr-val] (throw-nse))
328 |
329 | (deselect-all [n] (throw-nse))
330 |
331 | (deselect-by-index [n idx] (throw-nse))
332 |
333 | (deselect-by-text [n text] (throw-nse))
334 |
335 | (deselect-by-value [n value] (throw-nse))
336 |
337 | (first-selected-option [n] (throw-nse))
338 |
339 | (multiple? [n] (throw-nse))
340 |
341 | (select-option [n attr-val] (throw-nse))
342 |
343 | (select-all [n] (throw-nse))
344 |
345 | (select-by-index [n idx] (throw-nse))
346 |
347 | (select-by-text [n text] (throw-nse))
348 |
349 | (select-by-value [n value] (throw-nse)))
350 |
351 | (extend-protocol IFind
352 | nil
353 | (find-element-by [n by] (throw-nse))
354 |
355 | (find-elements-by [n by] (throw-nse))
356 |
357 | (find-element [n by] (throw-nse))
358 |
359 | (find-elements [n by] (throw-nse)))
360 |
--------------------------------------------------------------------------------
/src/webdriver/core_driver.clj:
--------------------------------------------------------------------------------
1 | ;; ## Core WebDriver-related Functions ##
2 |
3 | ;; This namespace provides the implementations for the following
4 | ;; protocols:
5 |
6 | ;; * IDriver
7 | ;; * ITargetLocator
8 | ;; * IAlert
9 | ;; * IOptions
10 | ;; * IFind
11 | (in-ns 'webdriver.core)
12 |
13 | (defn ^Actions new-actions
14 | "Create a new Actions object given a `WebDriver`"
15 | [^WebDriver wd]
16 | (Actions. wd))
17 |
18 | ;; Needed by window and target locator implementations in core_driver and core_window
19 | (defn ^WebDriver$Window window*
20 | "Return the underyling `WebDriver$Window` object for the `WebDriver`"
21 | [^WebDriver wd]
22 | (.window (.manage wd)))
23 |
24 | (defn key-code
25 | "Representations of pressable keys that aren't text. These are stored in the Unicode PUA (Private Use Area) code points, 0xE000-0xF8FF. Refer to http://www.google.com.au/search?&q=unicode+pua&btnG=Search"
26 | [k]
27 | (Keys/valueOf (.toUpperCase (name k))))
28 |
29 | ;; ## JavaScript Execution ##
30 | (defn execute-script*
31 | "Version of execute-script that uses a WebDriver instance directly."
32 | [^RemoteWebDriver webdriver js & js-args]
33 | (.executeScript webdriver ^String js (into-array Object js-args)))
34 |
35 | (defn execute-script
36 | [^WebDriver wd js & js-args]
37 | (apply execute-script* wd js js-args))
38 |
39 | (declare find-element* find-elements*)
40 |
41 | (extend-type WebDriver
42 |
43 | ;; Basic Functions
44 | IDriver
45 | (back [wd]
46 | (.back (.navigate wd))
47 | wd)
48 |
49 | (close [wd]
50 | (let [handles (into #{} (.getWindowHandles wd))]
51 | (when (> (count handles) 1) ; get back to a window that is open before proceeding
52 | (let [current-handle (.getWindowHandle wd)]
53 | (switch-to-window wd (first (disj handles current-handle)))))
54 | (.close wd)))
55 |
56 | (current-url [wd]
57 | (.getCurrentUrl wd))
58 |
59 | (forward [wd]
60 | (.forward (.navigate wd))
61 | wd)
62 |
63 | (get-url [wd url]
64 | (.get wd url)
65 | wd)
66 |
67 | (get-screenshot
68 | ([wd] (get-screenshot wd :file))
69 | ([wd format] (get-screenshot wd format nil))
70 | ([wd format destination]
71 | {:pre [(or (= format :file)
72 | (= format :base64)
73 | (= format :bytes))]}
74 | (let [wd ^TakesScreenshot wd
75 | output (case format
76 | :file (.getScreenshotAs wd OutputType/FILE)
77 | :base64 (.getScreenshotAs wd OutputType/BASE64)
78 | :bytes (.getScreenshotAs wd OutputType/BYTES))]
79 | (if destination
80 | (do
81 | (io/copy output (io/file destination))
82 | (log/info "Screenshot written to destination")
83 | output)
84 | output))))
85 |
86 | (page-source [wd]
87 | (.getPageSource wd))
88 |
89 | (quit [wd]
90 | (.quit wd))
91 |
92 | (refresh [wd]
93 | (.refresh (.navigate wd))
94 | wd)
95 |
96 | (title [wd]
97 | (.getTitle wd))
98 |
99 | (to [wd ^String url]
100 | (.to (.navigate wd) url)
101 | wd)
102 |
103 |
104 | ;; Window and Frame Handling
105 | ITargetLocator
106 | (window [wd]
107 | (window* wd))
108 |
109 | (window-handle [wd]
110 | (.getWindowHandle wd))
111 |
112 | (window-handles [wd]
113 | (into #{} (.getWindowHandles wd)))
114 |
115 | (other-window-handles [wd]
116 | (let [handles (window-handles wd)]
117 | (disj handles (.getWindowHandle wd))))
118 |
119 | (switch-to-frame [wd frame]
120 | ;; reflection warnings
121 | (cond
122 | (string? frame) (.frame (.switchTo wd) ^String frame)
123 | (number? frame) (.frame (.switchTo wd) ^int frame)
124 | :else (.frame (.switchTo wd) ^WebElement frame))
125 | wd)
126 |
127 | (switch-to-window [wd window-handle]
128 | (.window (.switchTo wd) window-handle)
129 | wd)
130 |
131 | (switch-to-other-window [wd]
132 | (if (= (count (window-handles wd)) 2)
133 | (switch-to-window wd (first (other-window-handles wd)))
134 | (throw (ex-info "You may use this function iff two windows are open."
135 | {:window-handles (window-handles wd)}))))
136 |
137 | (switch-to-default [wd]
138 | (.defaultContent (.switchTo wd)))
139 |
140 | (switch-to-active [wd]
141 | (.activeElement (.switchTo wd)))
142 |
143 | ;; Options Interface (cookies)
144 | IOptions
145 | (add-cookie [wd cookie]
146 | (.addCookie (.manage wd) cookie)
147 | wd)
148 |
149 | (delete-cookie-named [wd cookie-name]
150 | (.deleteCookieNamed (.manage wd) cookie-name)
151 | wd)
152 |
153 | (delete-cookie [wd cookie]
154 | (.deleteCookie (.manage wd) cookie)
155 | wd)
156 |
157 | (delete-all-cookies [wd]
158 | (.deleteAllCookies (.manage wd))
159 | wd)
160 |
161 | (cookies [wd]
162 | (into #{} (.getCookies (.manage wd))))
163 |
164 | (cookie-named [wd cookie-name]
165 | (.getCookieNamed (.manage wd) cookie-name))
166 |
167 | ;; Alert dialogs
168 | IAlert
169 | (accept [wd]
170 | (.accept (.alert (.switchTo wd))))
171 |
172 | (alert-obj [wd]
173 | (.alert (.switchTo wd)))
174 |
175 | (alert-text [wd]
176 | (let [switch (.switchTo wd)
177 | alert (.alert switch)]
178 | (.getText alert)))
179 |
180 | ;; (authenticate-using [wd username password]
181 | ;; (let [creds (UserAndPassword. username password)]
182 | ;; (-> wd .switchTo .alert (.authenticateUsing creds))))
183 |
184 | (dismiss [wd]
185 | (.dismiss (.alert (.switchTo wd))))
186 |
187 | ;; Find Functions
188 | IFind
189 | (find-element-by [wd by-value]
190 | (let [by-value (if (map? by-value)
191 | (by-query (build-query by-value))
192 | by-value)]
193 | (.findElement wd by-value)))
194 |
195 | (find-elements-by [wd by-value]
196 | (let [by-value (if (map? by-value)
197 | (by-query (build-query by-value))
198 | by-value)]
199 | (.findElements wd by-value)))
200 |
201 | (find-table-cell [wd table coords]
202 | (when (not= (count coords) 2)
203 | (throw (IllegalArgumentException.
204 | (str "The `coordinates` parameter must be a seq with two items."))))
205 | (let [[row col] coords
206 | row-css (str "tr:nth-child(" (inc row) ")")
207 | col-css (if (and (find-element-by table (by-tag "th"))
208 | (zero? row))
209 | (str "th:nth-child(" (inc col) ")")
210 | (str "td:nth-child(" (inc col) ")"))
211 | complete-css (str row-css " " col-css)]
212 | (find-element-by table (by-query {:css complete-css}))))
213 |
214 | (find-table-row [wd table row]
215 | (let [row-css (str "tr:nth-child(" (inc row) ")")
216 | complete-css (if (and (find-element-by table (by-tag "th"))
217 | (zero? row))
218 | (str row-css " " "th")
219 | (str row-css " " "td"))]
220 | ;; WebElement, not WebDriver, version of protocol
221 | (find-elements-by table (by-query {:css complete-css}))))
222 |
223 | ;; TODO: reconsider find-table-col with CSS support
224 |
225 | (find-by-hierarchy [wd hierarchy-vec]
226 | (find-elements wd {:xpath (build-query hierarchy-vec)}))
227 |
228 | (find-elements
229 | ([wd attr-val]
230 | (find-elements* wd attr-val)))
231 |
232 | (find-element
233 | ([wd attr-val]
234 | (find-element* wd attr-val)))
235 |
236 | IActions
237 |
238 | (click-and-hold
239 | ([wd]
240 | (let [act (new-actions wd)]
241 | (.perform (.clickAndHold act))))
242 | ([wd webelement]
243 | (let [act (new-actions wd)]
244 | (.perform (.clickAndHold act webelement)))))
245 |
246 | (double-click
247 | ([wd]
248 | (let [act (new-actions wd)]
249 | (.perform (.doubleClick act))))
250 | ([wd webelement]
251 | (let [act (new-actions wd)]
252 | (.perform (.doubleClick act webelement)))))
253 |
254 | (drag-and-drop
255 | [wd webelement-a webelement-b]
256 | (cond
257 | (nil? webelement-a) (throw-nse "The first element does not exist.")
258 | (nil? webelement-b) (throw-nse "The second element does not exist.")
259 | :else (let [act (new-actions wd)]
260 | (.perform (.dragAndDrop act
261 | webelement-a
262 | webelement-b)))))
263 |
264 | (drag-and-drop-by
265 | [wd webelement x-y-map]
266 | (if (nil? webelement)
267 | (throw-nse)
268 | (let [act (new-actions wd)
269 | {:keys [x y] :or {x 0 y 0}} x-y-map]
270 | (.perform
271 | (.dragAndDropBy act webelement x y)))))
272 |
273 | (key-down
274 | ([wd k]
275 | (let [act (new-actions wd)]
276 | (.perform (.keyDown act (key-code k)))))
277 | ([wd webelement k]
278 | (let [act (new-actions wd)]
279 | (.perform (.keyDown act webelement (key-code k))))))
280 |
281 | (key-up
282 | ([wd k]
283 | (let [act (new-actions wd)]
284 | (.perform (.keyUp act (key-code k)))))
285 | ([wd webelement k]
286 | (let [act (new-actions wd)]
287 | (.perform (.keyUp act webelement (key-code k))))))
288 |
289 | (move-by-offset
290 | [wd x y]
291 | (let [act (new-actions wd)]
292 | (.perform (.moveByOffset act x y))))
293 |
294 | (move-to-element
295 | ([wd webelement]
296 | (let [act (new-actions wd)]
297 | (.perform (.moveToElement act webelement))))
298 | ([wd webelement x y]
299 | (let [act (new-actions wd)]
300 | (.perform (.moveToElement act webelement x y)))))
301 |
302 | (release
303 | ([wd]
304 | (let [act (new-actions wd)]
305 | (.release act)))
306 | ([wd element]
307 | (let [act (new-actions wd)]
308 | (.release act element)))))
309 |
310 | (extend-type Actions
311 |
312 | IActions
313 | ;; TODO: test coverage
314 | (click-and-hold
315 | ([act]
316 | (.clickAndHold act))
317 | ([act webelement]
318 | (.clickAndHold act webelement)))
319 |
320 | ;; TODO: test coverage
321 | (double-click
322 | ([act]
323 | (.doubleClick act))
324 | ([act webelement]
325 | (.doubleClick act webelement)))
326 |
327 | ;; TODO: test coverage
328 | (drag-and-drop
329 | [act webelement-a webelement-b]
330 | (.dragAndDrop act webelement-a webelement-b))
331 |
332 | ;; TODO: test coverage
333 | (drag-and-drop-by
334 | [act webelement x y]
335 | (.dragAndDropBy act webelement x y))
336 |
337 | ;; TODO: test coverage
338 | (key-down
339 | ([act k]
340 | (.keyDown act (key-code k)))
341 | ([act webelement k]
342 | (.keyDown act webelement (key-code k))))
343 |
344 | ;; TODO: test coverage
345 | (key-up
346 | ([act k]
347 | (.keyUp act (key-code k)))
348 | ([act webelement k]
349 | (.keyUp act webelement (key-code k))))
350 |
351 | ;; TODO: test coverage
352 | (move-by-offset
353 | [act x y]
354 | (.moveByOffset act x y))
355 |
356 | ;; TODO: test coverage
357 | (move-to-element
358 | ([act webelement]
359 | (.moveToElement act webelement))
360 | ([act webelement x y]
361 | (.moveToElement act webelement x y)))
362 |
363 | ;; TODO: test coverage
364 | (perform [act] (.perform act))
365 |
366 | ;; TODO: test coverage
367 | (release
368 | ([act]
369 | (.release act))
370 | ([act webelement]
371 | (.release act webelement))))
372 |
373 | (extend-type CompositeAction
374 |
375 | IActions
376 | (perform [comp-act] (.perform comp-act)))
377 |
378 | (defn find-element* [wd attr-val]
379 | (first (find-elements wd attr-val)))
380 |
381 | (defn find-elements* [wd attr-val]
382 | (when (seq attr-val)
383 | (try
384 | (cond
385 | ;; Accept by-clauses
386 | (instance? By attr-val)
387 | (find-elements-by wd attr-val)
388 |
389 | ;; Accept vectors for hierarchical queries
390 | (vector? attr-val)
391 | (find-by-hierarchy wd attr-val)
392 |
393 | ;; Build CSS/XPath dynamically
394 | :else
395 | (find-elements-by wd (by-query (build-query attr-val))))
396 | (catch org.openqa.selenium.NoSuchElementException e
397 | ;; NoSuchElementException caught here to mimic Clojure behavior like
398 | ;; (get {:foo "bar"} :baz) since the page can be thought of as a kind of associative
399 | ;; data structure with unique selectors as keys and HTML elements as values
400 | nil))))
401 |
--------------------------------------------------------------------------------
/src/webdriver/util.clj:
--------------------------------------------------------------------------------
1 | (ns webdriver.util
2 | (:require [clojure.string :as str]
3 | [clojure.java.io :as io]
4 | [clojure.walk :as walk])
5 | (:import [java.io PushbackReader Writer]
6 | [org.openqa.selenium Capabilities HasCapabilities
7 | WebDriver WebElement NoSuchElementException]))
8 |
9 | (declare build-query)
10 |
11 | (defn build-css-attrs
12 | "Given a map of attribute-value pairs, build the latter portion of a CSS query that follows the tag."
13 | [attr-val]
14 | (clojure.string/join (for [[attr value] attr-val]
15 | (cond
16 | (= :text attr) (throw (IllegalArgumentException. "CSS queries do not support checking against the text of an element."))
17 | (= :index attr) (str ":nth-child(" (inc value) ")") ;; CSS is 1-based
18 | :else (str "[" (name attr) "='" value "']")))))
19 |
20 | (defn build-xpath-attrs
21 | "Given a map of attribute-value pairs, build the bracketed portion of an XPath query that follows the tag"
22 | [attr-val]
23 | (clojure.string/join (for [[attr value] attr-val]
24 | (cond
25 | (= :text attr) (str "[text()=\"" value "\"]")
26 | (= :index attr) (str "[" (inc value) "]") ; in clj-webdriver,
27 | :else (str "[@" ; all indices 0-based
28 | (name attr)
29 | "="
30 | "'" (name value) "']")))))
31 |
32 | (defn build-css-with-hierarchy
33 | "Given a vector of queries in hierarchical order, create a CSS query.
34 | For example: `[{:tag :div, :id \"content\"}, {:tag :a, :class \"external\"}]` would
35 | produce the CSS query \"div[id='content'] a[class='external']\""
36 | [v-of-attr-vals]
37 | (str/join
38 | " "
39 | (for [attr-val v-of-attr-vals]
40 | (cond
41 | (or (contains? attr-val :css)
42 | (contains? attr-val :xpath)) (throw (IllegalArgumentException. "Hierarhical queries do not support the use of :css or :xpath entries."))
43 | (some #{(:tag attr-val)} [:radio
44 | :checkbox
45 | :textfield
46 | :password
47 | :filefield]) (throw (IllegalArgumentException. "Hierarchical queries do not support the use of \"meta\" tags such as :button*, :radio, :checkbox, :textfield, :password or :filefield. "))
48 |
49 | :else (:css (build-query attr-val :css))))))
50 |
51 | (defn build-xpath-with-hierarchy
52 | "Given a vector of queries in hierarchical order, create XPath.
53 | For example: `[{:tag :div, :id \"content\"}, {:tag :a, :class \"external\"}]` would
54 | produce the XPath \"//div[@id='content']//a[@class='external']"
55 | [v-of-attr-vals]
56 | (clojure.string/join (for [attr-val v-of-attr-vals]
57 | (cond
58 | (or (contains? attr-val :css)
59 | (contains? attr-val :xpath)) (throw (IllegalArgumentException. "Hierarhical queries do not support the use of :css or :xpath entries."))
60 | (some #{(:tag attr-val)} [:radio
61 | :checkbox
62 | :textfield
63 | :password
64 | :filefield]) (throw (IllegalArgumentException. "Hierarchical queries do not support the use of \"meta\" tags such as :button*, :radio, :checkbox, :textfield, :password or :filefield. "))
65 | :else (:xpath (build-query attr-val))))))
66 |
67 |
68 | (declare remove-regex-entries)
69 | (defn build-query
70 | "Given a map of attribute-value pairs, generate XPath or CSS based on `output`. Optionally include a `prefix` to specify whether this should be a `:global` \"top-level\" query or a `:local`, child query."
71 | ([attr-val] (build-query attr-val :xpath :global))
72 | ([attr-val output] (build-query attr-val output :global))
73 | ([attr-val output prefix]
74 | (if-not (map? attr-val) ;; dispatch here for hierarhical queries
75 | (if (= output :xpath)
76 | (build-xpath-with-hierarchy attr-val)
77 | (build-css-with-hierarchy attr-val))
78 | (let [attr-val (remove-regex-entries attr-val)]
79 | (cond
80 | (contains? attr-val :xpath) {:xpath (:xpath attr-val)}
81 | (contains? attr-val :css) {:css (:css attr-val)}
82 | (= (:tag attr-val) :radio) (build-query (assoc attr-val :tag :input :type "radio"))
83 | (= (:tag attr-val) :checkbox) (build-query (assoc attr-val :tag :input :type "checkbox"))
84 | (= (:tag attr-val) :textfield) (build-query (assoc attr-val :tag :input :type "text"))
85 | (= (:tag attr-val) :password) (build-query (assoc attr-val :tag :input :type "password"))
86 | (= (:tag attr-val) :filefield) (build-query (assoc attr-val :tag :input :type "filefield"))
87 | :else (let [tag (if (nil? (:tag attr-val))
88 | :*
89 | (:tag attr-val))
90 | attr-val (dissoc attr-val :tag)
91 | prefix-legend {:local "."
92 | :global ""}]
93 | (if (= output :xpath)
94 | (let [query-str (str (prefix-legend prefix) "//"
95 | (name tag)
96 | (when (seq attr-val)
97 | (build-xpath-attrs attr-val)))]
98 | {:xpath query-str})
99 | ;; else, CSS
100 | (let [query-str (str (name tag)
101 | (when (seq attr-val)
102 | (build-css-attrs attr-val)))]
103 | {:css query-str}))))))))
104 |
105 |
106 |
107 | (defn contains-regex?
108 | "Checks if the values of a map contain a regex"
109 | [m]
110 | (boolean (some (fn [entry]
111 | (let [[k v] entry]
112 | (= java.util.regex.Pattern (class v)))) m)))
113 |
114 | (defn all-regex?
115 | "Checks if all values of a map are regexes"
116 | [m]
117 | (and (seq m)
118 | (not-any? (fn [entry]
119 | (let [[k v] entry]
120 | (not= java.util.regex.Pattern (class v)))) m)))
121 |
122 | (defn filter-regex-entries
123 | "Given a map `m`, return a map containing only entries whose values are regular expressions."
124 | [m]
125 | (into {} (filter
126 | #(let [[k v] %] (= java.util.regex.Pattern (class v)))
127 | m)))
128 |
129 | (defn remove-regex-entries
130 | "Given a map `m`, return a map containing only entries whose values are NOT regular expressions."
131 | [m]
132 | (into {} (remove
133 | #(let [[k v] %] (= java.util.regex.Pattern (class v)))
134 | m)))
135 |
136 | (defn first-n-chars
137 | "Get first n characters of `s`, then add ellipsis"
138 | ([s] (first-n-chars s 60))
139 | ([s n]
140 | (if (zero? n)
141 | "..."
142 | (str (re-find (re-pattern (str "(?s).{1," n "}")) s)
143 | (when (> (count s) n)
144 | "...")))))
145 |
146 | (defn elim-breaks
147 | "Eliminate line breaks; used for REPL printing"
148 | [s]
149 | (str/replace s #"(\r|\n|\r\n)" " "))
150 |
151 | (defmacro when-attr
152 | "Special `when` macro for checking if an attribute isn't available or is an empty string"
153 | [obj & body]
154 | `(when (not (or (nil? ~obj) (empty? ~obj)))
155 | ~@body))
156 |
157 | ;; from Clojure's core.clj
158 | (defmacro assert-args
159 | [& pairs]
160 | `(do (when-not ~(first pairs)
161 | (throw (IllegalArgumentException.
162 | (str (first ~'&form) " requires " ~(second pairs) " in " ~'*ns* ":" (:line (meta ~'&form))))))
163 | ~(let [more (nnext pairs)]
164 | (when more
165 | (list* `assert-args more)))))
166 |
167 | ;; from Clojure's core.clj
168 | (defn pr-on
169 | {:private true
170 | :static true}
171 | [x w]
172 | (if *print-dup*
173 | (print-dup x w)
174 | (print-method x w))
175 | nil)
176 |
177 | ;; from Clojure core_print.clj
178 | (defn- print-sequential [^String begin, print-one, ^String sep, ^String end, sequence, ^Writer w]
179 | (binding [*print-level* (and (not *print-dup*) *print-level* (dec *print-level*))]
180 | (if (and *print-level* (neg? *print-level*))
181 | (.write w "#")
182 | (do
183 | (.write w begin)
184 | (when-let [xs (seq sequence)]
185 | (if (and (not *print-dup*) *print-length*)
186 | (loop [[x & xs] xs
187 | print-length *print-length*]
188 | (if (zero? print-length)
189 | (.write w "...")
190 | (do
191 | (print-one x w)
192 | (when xs
193 | (.write w sep)
194 | (recur xs (dec print-length))))))
195 | (loop [[x & xs] xs]
196 | (print-one x w)
197 | (when xs
198 | (.write w sep)
199 | (recur xs)))))
200 | (.write w end)))))
201 |
202 | ;; from Clojure core_print.clj
203 | (defn- print-map [m print-one w]
204 | (print-sequential
205 | "{"
206 | (fn [e ^Writer w]
207 | (do (print-one (key e) w) (.append w \space) (print-one (val e) w)))
208 | ", "
209 | "}"
210 | (seq m) w))
211 |
212 | ;; from Clojure core_print.clj
213 | (defn- print-meta [o, ^Writer w]
214 | (when-let [m (meta o)]
215 | (when (and (pos? (count m))
216 | (or *print-dup*
217 | (and *print-meta* *print-readably*)))
218 | (.write w "^")
219 | (if (and (= (count m) 1) (:tag m))
220 | (pr-on (:tag m) w)
221 | (pr-on m w))
222 | (.write w " "))))
223 |
224 | (defmethod print-method WebDriver
225 | [^WebDriver q w]
226 | (let [^Capabilities caps (.getCapabilities ^HasCapabilities q)]
227 | (print-simple
228 | (str "#<" "Title: " (.getTitle q) ", "
229 | "URL: " (first-n-chars (.getCurrentUrl q)) ", "
230 | "Browser: " (.getBrowserName caps) ", "
231 | "Version: " (.getVersion caps) ", "
232 | "JS Enabled: " (.isJavascriptEnabled caps) ", "
233 | "Native Events Enabled: " (boolean (re-find #"nativeEvents=true" (str caps))) ", "
234 | "Object: " q ">") w)))
235 |
236 | (defmethod print-method WebElement
237 | [^WebElement q w]
238 | (let [tag-name (.getTagName q)
239 | text (.getText q)
240 | id (.getAttribute q "id")
241 | class-name (.getAttribute q "class")
242 | name-name (.getAttribute q "name")
243 | value (.getAttribute q "value")
244 | href (.getAttribute q "href")
245 | src (.getAttribute q "src")
246 | obj q]
247 | (print-simple
248 | (str "#<"
249 | (when-attr tag-name
250 | (str "Tag: " "<" tag-name ">" ", "))
251 | (when-attr text
252 | (str "Text: " (-> text elim-breaks first-n-chars) ", "))
253 | (when-attr id
254 | (str "Id: " id ", "))
255 | (when-attr class-name
256 | (str "Class: " class-name ", "))
257 | (when-attr name-name
258 | (str "Name: " name-name ", "))
259 | (when-attr value
260 | (str "Value: " (-> value elim-breaks first-n-chars) ", "))
261 | (when-attr href
262 | (str "Href: " href ", "))
263 | (when-attr src
264 | (str "Source: " src ", "))
265 | "Object: " q ">") w)))
266 |
267 | (defn dashes-to-camel-case
268 | "A simple conversion of `-x` to `X` for the given string."
269 | [s]
270 | (reduce (fn [^String state ^String item]
271 | (.replaceAll state item
272 | (str/upper-case (str (second item)))))
273 | s
274 | (distinct (re-seq #"-[^-]" s))))
275 |
276 | (defn camel-case-to-dashes
277 | "Convert Pascal-case to dashes. This takes into account edge cases like `fooJSBar` and `fooBarB`, where dashed versions will be `foo-jS-bar` and `foo-barB` respectively."
278 | [s]
279 | (reduce (fn [^String state ^String item]
280 | ;; e.g.: state = trustAllSSLCertificates
281 | ;; item can be either "tA" or "lSSLC"
282 | (if (= (count item) 2)
283 | (.replaceFirst state item
284 | (str (first item)
285 | "-"
286 | (str/lower-case (second item))))
287 | (.replaceFirst state item
288 | (str (first item)
289 | "-"
290 | (str/lower-case (second item))
291 | (subs item 2 (dec (count item)))
292 | "-"
293 | (str/lower-case (last item))))))
294 | s
295 | (re-seq #"[a-z]?[A-Z]+(?:(?!$))" s)
296 | ;; (re-seq #"[a-z]?[A-Z]+" s)
297 | ;; (re-seq #"[a-z][A-Z](?![A-Z]|$)" s)
298 | ))
299 |
300 | (defn clojure-keys
301 | "Recursively transforms all map keys from strings to keywords, converting Pascal-case to dash-separated."
302 | [m]
303 | (let [f (fn [[k v]] (if (string? k) [(keyword (camel-case-to-dashes k)) v] [k v]))]
304 | ;; only apply to maps
305 | (walk/postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m)))
306 |
307 | (defn java-keys
308 | "Recursively transforms all map keys from keywords into strings, converting dash-separated to Pascal-case."
309 | [m]
310 | (let [f (fn [[k v]] (if (keyword? k) [(dashes-to-camel-case (name k)) v] [k v]))]
311 | ;; only apply to maps
312 | (walk/postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m)))
313 |
314 | (defn throw-nse
315 | ([] (throw-nse ""))
316 | ([msg]
317 | (throw (NoSuchElementException. (str msg "\n" "When an element cannot be found in clj-webdriver, nil is returned. You've just tried to perform an action on an element that returned as nil for the search query you used. Please verify the query used to locate this element; it is not on the current page.")))))
318 |
--------------------------------------------------------------------------------
/src/webdriver/core.clj:
--------------------------------------------------------------------------------
1 | ;; # Clojure API for Selenium-WebDriver #
2 | ;;
3 | ;; WebDriver is a library that allows for easy manipulation of the Firefox,
4 | ;; Chrome, Safari and Internet Explorer graphical browsers, as well as the
5 | ;; Java-based HtmlUnit headless browser.
6 | ;;
7 | ;; This library provides both a thin wrapper around WebDriver and a more
8 | ;; Clojure-friendly API for finding elements on the page and performing
9 | ;; actions on them. See the README for more details.
10 | ;;
11 | ;; Credits to mikitebeka's `webdriver-clj` project on Github for a starting-
12 | ;; point for this project and many of the low-level wrappers around the
13 | ;; WebDriver API.
14 | ;;
15 | (ns webdriver.core
16 | (:require [clojure.string :as string]
17 | [clojure.walk :refer [keywordize-keys]]
18 | [clojure.java.io :as io]
19 | [clojure.tools.logging :as log]
20 | [webdriver.js.browserbot :as browserbot-js]
21 | [webdriver.firefox :as ff]
22 | [webdriver.util :refer :all])
23 | (:import
24 | [java.lang.reflect Constructor Field]
25 | java.util.concurrent.TimeUnit
26 | [org.openqa.selenium By Capabilities Dimension Keys NoSuchElementException OutputType Point TakesScreenshot WebDriver WebElement WebDriver$Window]
27 | org.openqa.selenium.chrome.ChromeDriver
28 | [org.openqa.selenium.firefox FirefoxDriver FirefoxProfile]
29 | org.openqa.selenium.htmlunit.HtmlUnitDriver
30 | org.openqa.selenium.ie.InternetExplorerDriver
31 | [org.openqa.selenium.interactions Actions CompositeAction]
32 | [org.openqa.selenium.internal Locatable WrapsDriver]
33 | [org.openqa.selenium.remote DesiredCapabilities RemoteWebDriver]
34 | [org.openqa.selenium.support.ui ExpectedCondition Select WebDriverWait]))
35 |
36 | ;; ## Protocols for webdriver API ##
37 |
38 | ;; ### WebDriver Functions ###
39 | (defprotocol IDriver
40 | "Basics of driver handling"
41 | (back [driver] "Go back to the previous page in \"browsing history\"")
42 | (close [driver] "Close this browser instance, switching to an active one if more than one is open")
43 | (current-url [driver] "Retrieve the URL of the current page")
44 | (forward [driver] "Go forward to the next page in \"browsing history\".")
45 | (get-screenshot [driver] [driver format] [driver format destination] "Take a screenshot using Selenium-WebDriver's getScreenshotAs method")
46 | (get-url [driver url] "Navigate the driver to a given URL")
47 | (page-source [driver] "Retrieve the source code of the current page")
48 | (quit [driver] "Destroy this browser instance")
49 | (refresh [driver] "Refresh the current page")
50 | (title [driver] "Retrieve the title of the current page as defined in the `head` tag")
51 | (to [driver url] "Navigate to a particular URL. Arg `url` can be either String or java.net.URL. Equivalent to the `get` function, provided here for compatibility with WebDriver API."))
52 |
53 | ;; ### Windows and Frames ###
54 | (defprotocol ITargetLocator
55 | "Functions that deal with browser windows and frames"
56 | (window [driver] "Get the only (or first) window object. This is different from the string window handles that most of Selenium-WebDriver's API expects.")
57 | (window-handle [driver] "Retrieve this driver's window handle (defaults to only or active window).")
58 | (window-handles [driver] "Retrieve a vector of `Window` records which can be used to switch to particular open windows")
59 | (other-window-handles [driver] "Retrieve window handles for all windows except the current one")
60 | (switch-to-frame [driver frame] "Switch focus to a particular HTML frame by supplying a `WebElement` or an integer for the nth frame on the page (zero-based index)")
61 | (switch-to-window [driver handle] "Switch focus to a particular open window")
62 | (switch-to-other-window [driver] "Given that two and only two browser windows are open, switch to the one not currently active")
63 | (switch-to-default [driver] "Switch focus to the first first frame of the page, or the main document if the page contains iframes")
64 | (switch-to-active [driver] "Switch to element that currently has focus, or to the body if this cannot be detected"))
65 |
66 | (defprotocol IWait
67 | "Implicit and explicit waiting"
68 | (implicit-wait [wd timeout] "Specify the amount of time the WebDriver should wait when searching for an element if it is not immediately present. This setting holds for the lifetime of the driver across all requests. Units in milliseconds.")
69 | (wait-until
70 | [wd pred]
71 | [wd pred timeout]
72 | [wd pred timeout interval] "Set an explicit wait time `timeout` for a particular condition `pred`. Optionally set an `interval` for testing the given predicate. All units in milliseconds"))
73 |
74 | (defprotocol IWindow
75 | "Functions to manage browser size and position."
76 | (maximize [this] "Maximizes the current window to fit screen if it is not already maximized. Returns driver or window.")
77 | (position [this] "Returns map of X Y coordinates ex. {:x 1 :y 3} relative to the upper left corner of screen.")
78 | (reposition [this coordinates-map] "Excepts map of X Y coordinates ex. {:x 1 :y 3} repositioning current window relative to screen. Returns driver or window.")
79 | (resize [this dimensions-map] "Resize the driver window with a map of width and height ex. {:width 480 :height 800}. Returns driver or window.")
80 | (window-size [this] "Get size of current window. Returns a map of width and height ex. {:width 480 :height 800}"))
81 |
82 | (defprotocol IOptions
83 | "Options interface, including cookie and timeout handling"
84 | (add-cookie [driver cookie] "Add a new cookie to the browser session")
85 | (delete-cookie-named [driver cookie-name] "Delete a cookie given its name")
86 | (delete-cookie [driver cookie] "Delete a cookie given a cookie instance")
87 | (delete-all-cookies [driver] "Delete all cookies defined in the current session")
88 | (cookies [driver] "Retrieve a set of cookies defined in the current session")
89 | (cookie-named [driver cookie-name] "Retrieve a cookie object given its name"))
90 |
91 | ;; ### Alert Popups ###
92 | (defprotocol IAlert
93 | "Simple interactions with alert popups"
94 | (accept [driver] "Accept the dialog. Equivalent to pressing 'Ok'")
95 | (alert-obj [driver] "Return the underlying Java object that can be used with the Alert Java API (exposed until all functionality is ported)")
96 | (alert-text [driver] "Get the text of the popup dialog's message")
97 | ;; (authenticate-using [driver username password] "Enter `username` and `password` into fields from a Basic Access Authentication popup dialog")
98 | (dismiss [driver] "Dismiss the dialog. Equivalent to pressing 'Cancel'"))
99 |
100 | ;; ### Finding Elements on Page ###
101 | (defprotocol IFind
102 | "Functions used to locate elements on a given page"
103 | (find-element-by [this by] "Retrieve the element object of an element described by `by`, optionally limited to elements beneath a parent element (depends on dispatch). Prefer `find-element` to this function unless you know what you're doing.")
104 | (find-elements-by [this by] "Retrieve a seq of element objects described by `by`, optionally limited to elements beneath a parent element (depends on dispatch). Prefer `find-elements` to this function unless you know what you're doing.")
105 | (find-table-cell [driver table coordinates] "Given a `driver`, a `table` element, and a zero-based set of coordinates for row and column, return the table cell at those coordinates for the given table.")
106 | (find-table-row [driver table row-index] "Return all cells in the row of the given table element, `row-index` as a zero-based index of the target row.")
107 | (find-by-hierarchy [driver hierarchy-vector] "Given a Webdriver `driver` and a vector `hierarchy-vector`, return a sequence of the described elements in the hierarchy dictated by the order of elements in the `hierarchy-vector`.")
108 | (find-elements [this locator] "Find all elements that match the parameters supplied in the `attr-val` map. Also provides a shortcut to `find-by-hierarchy` if a vector is supplied instead of a map.")
109 | (find-element [this locator] "Call (first (find-elements args))"))
110 |
111 | ;; ### Acting on Elements ###
112 | (defprotocol IElement
113 | "Basic actions on elements"
114 | (attribute [element attr] "Retrieve the value of the attribute of the given element object")
115 | (click [element] "Click a particular HTML element")
116 | (css-value [element property] "Return the value of the given CSS property")
117 | (displayed? [element] "Returns true if the given element object is visible/displayed")
118 | (exists? [element] "Returns true if the given element exists")
119 | (flash [element] "Flash the element in question, to verify you're looking at the correct element")
120 | (focus [element] "Apply focus to the given element")
121 | (html [element] "Retrieve the outer HTML of an element")
122 | (intersects? [element-a element-b] "Return true if `element-a` intersects with `element-b`. This mirrors the Selenium-WebDriver API method, but see the `intersect?` function to compare an element against multiple other elements for intersection.")
123 | (location-on-page [element] "Given an element object, return its absolute location as a map of its x/y coordinates with the top-left of the page as origin.")
124 | (location-in-viewport [element] "Given an element object, return its relative location as a map of its x/y coordinates based on where the element is in the viewport, or once it has been scrolled into view.")
125 | (present? [element] "Returns true if the element exists and is visible")
126 | (element-size [element] "Return the size of the given `element` as a map containing `:width` and `:height` values in pixels.")
127 | (tag [element] "Retrieve the name of the HTML tag of the given element object (returned as a keyword)")
128 | (text [element] "Retrieve the content, or inner HTML, of a given element object")
129 | (value [element] "Retrieve the `value` attribute of the given element object")
130 | (visible? [element] "Returns true if the given element object is visible/displayed")
131 | (xpath [element] "Retrieve the XPath of an element"))
132 |
133 | ;; ### Acting on Form-Specific Elements ###
134 | (defprotocol IFormElement
135 | "Actions for form elements"
136 | (clear [element] "Clear the contents of the given element object")
137 | (deselect [element] "Deselect a given element object")
138 | (enabled? [element] "Returns true if the given element object is enabled")
139 | (input-text [element s] "Type the string of keys into the element object")
140 | (submit [element] "Submit the form which contains the given element object")
141 | (select [element] "Select a given element object")
142 | (selected? [element] "Returns true if the given element object is selected")
143 | (send-keys [element s] "Type the string of keys into the element object")
144 | (toggle [element] "If the given element object is a checkbox, this will toggle its selected/unselected state. In Selenium 2, `.toggle()` was deprecated and replaced in usage by `.click()`."))
145 |
146 | ;; ### Acting on Select Elements ###
147 | (defprotocol ISelectElement
148 | "Actions specific to select lists"
149 | (all-options [select-element] "Retrieve all options from the given select list")
150 | (all-selected-options [select-element] "Retrieve a seq of all selected options from the select list described by `by`")
151 | (deselect-option [select-element attr-val] "Deselect an option from a select list, either by `:value`, `:index` or `:text`")
152 | (deselect-all [select-element] "Deselect all options for a given select list. Does not leverage WebDriver method because WebDriver's isMultiple method is faulty.")
153 | (deselect-by-index [select-element idx] "Deselect the option at index `idx` for the select list described by `by`. Indexes begin at 0")
154 | (deselect-by-text [select-element text] "Deselect all options with visible text `text` for the select list described by `by`")
155 | (deselect-by-value [select-element value] "Deselect all options with value `value` for the select list described by `by`")
156 | (first-selected-option [select-element] "Retrieve the first selected option (or the only one for single-select lists) from the given select list")
157 | (multiple? [select-element] "Return true if the given select list allows for multiple selections")
158 | (select-option [select-element attr-val] "Select an option from a select list, either by `:value`, `:index` or `:text`")
159 | (select-all [select-element] "Select all options for a given select list")
160 | (select-by-index [select-element idx] "Select an option by its index in the given select list. Indexes begin at 0.")
161 | (select-by-text [select-element text] "Select all options with visible text `text` in the select list described by `by`")
162 | (select-by-value [select-element value] "Select all options with value `value` in the select list described by `by`"))
163 |
164 | (defprotocol IActions
165 | "Methods available in the Actions class"
166 | (click-and-hold
167 | [this]
168 | [this element] "Drag and drop, either at the current mouse position or in the middle of a given `element`.")
169 | (double-click
170 | [this]
171 | [this element] "Double click, either at the current mouse position or in the middle of a given `element`.")
172 | (drag-and-drop [this element-a element-b] "Drag and drop `element-a` onto `element-b`.")
173 | (drag-and-drop-by [this element x-y-map] "Drag `element` by `x` pixels to the right and `y` pixels down.")
174 | (key-down
175 | [this k]
176 | [this element k] "Press the given key (e.g., (key-press driver :enter))")
177 | (key-up
178 | [this k]
179 | [this element k] "Release the given key (e.g., (key-press driver :enter))")
180 | (move-by-offset [driver x y] "Move mouse by `x` pixels to the right and `y` pixels down.")
181 | (move-to-element
182 | [this element]
183 | [this element x y] "Move the mouse to the given element, or to an offset from the given element.")
184 | (perform [this] "Perform the composite action chain.")
185 | (release
186 | [this]
187 | [this element] "Release the left mouse button, either at the current mouse position or in the middle of the given `element`."))
188 |
189 | ;; ## Starting Browser ##
190 | (def ^{:doc "Map of keywords to available WebDriver classes."}
191 | webdriver-drivers
192 | {:firefox FirefoxDriver
193 | :ie InternetExplorerDriver
194 | :internet-explorer InternetExplorerDriver
195 | :chrome ChromeDriver
196 | :chromium ChromeDriver
197 | :htmlunit HtmlUnitDriver})
198 |
199 | (def phantomjs-enabled?
200 | (try
201 | (import '[org.openqa.selenium.phantomjs PhantomJSDriver PhantomJSDriverService])
202 | true
203 | (catch Throwable _ false)))
204 |
205 | (defmulti new-webdriver
206 | "Return a Selenium-WebDriver WebDriver instance, with particularities of each browser supported."
207 | :browser)
208 |
209 | (defmethod new-webdriver :default
210 | [{:keys [browser]}]
211 | (let [^Class klass (or (browser webdriver-drivers) browser)]
212 | (.newInstance
213 | (.getConstructor klass (into-array Class []))
214 | (into-array Object []))))
215 |
216 | (defmethod new-webdriver :firefox
217 | [{:keys [browser ^FirefoxProfile profile]}]
218 | (if profile
219 | (FirefoxDriver. profile)
220 | (FirefoxDriver.)))
221 |
222 | (defmethod new-webdriver :phantomjs
223 | [{:keys [phantomjs-executable] :as browser-spec}]
224 | (if-not phantomjs-enabled?
225 | (throw (RuntimeException. "You do not have the PhantomJS JAR's on the classpath. Please add com.codeborne/phantomjsdriver version 1.2.1 with exclusions for org.seleniumhq.selenium/selenium-java and any other org.seleniumhq.selenium JAR's your code relies on."))
226 | (let [caps (DesiredCapabilities.)
227 | klass (Class/forName "org.openqa.selenium.phantomjs.PhantomJSDriver")
228 | ;; Second constructor takes single argument of Capabilities
229 | ctors (into [] (.getDeclaredConstructors klass))
230 | ctor-sig (fn [^Constructor ctor]
231 | (let [param-types (.getParameterTypes ctor)]
232 | (and (= (alength param-types) 1)
233 | (= Capabilities (aget param-types 0)))))
234 | phantomjs-driver-ctor (first (filterv ctor-sig ctors))]
235 | ;; Seems to be able to find it if on PATH by default, like Chrome's driver
236 | (when phantomjs-executable
237 | (let [klass (Class/forName "org.openqa.selenium.phantomjs.PhantomJSDriverService")
238 | field (.getField klass "PHANTOMJS_EXECUTABLE_PATH_PROPERTY")]
239 | (.setCapability ^DesiredCapabilities caps
240 | ^String (.get field klass)
241 | ^String phantomjs-executable)))
242 | (.newInstance ^Constructor phantomjs-driver-ctor (into-array java.lang.Object [caps])))))
243 |
244 | ;; Borrowed from core Clojure
245 | (defmacro with-driver
246 | "Given a binding to `WebDriver`, make that binding available in `body` and ensure `quit` is called on it at the end."
247 | [bindings & body]
248 | (assert-args
249 | (vector? bindings) "a vector for its binding"
250 | (even? (count bindings)) "an even number of forms in binding vector")
251 | (cond
252 | (zero? (count bindings)) `(do ~@body)
253 | (symbol? (bindings 0)) `(let ~(subvec bindings 0 2)
254 | (try
255 | (with-driver ~(subvec bindings 2) ~@body)
256 | (finally
257 | (quit ~(bindings 0)))))
258 | :else (throw (IllegalArgumentException.
259 | "with-driver only allows symbols in bindings"))))
260 |
261 | (load "core_by")
262 | (load "core_element")
263 | (load "core_wait")
264 | (load "core_driver")
265 | (load "core_window")
266 | (load "core_actions")
267 |
--------------------------------------------------------------------------------
/test/webdriver/test/common.clj:
--------------------------------------------------------------------------------
1 | ;; Namespace with implementations of test cases
2 | (ns webdriver.test.common
3 | (:require [clojure.test :refer :all]
4 | [clojure.string :refer [lower-case]]
5 | [clojure.java.io :as io]
6 | [clojure.tools.logging :as log]
7 | [webdriver.test.helpers :refer :all]
8 | [webdriver.core :refer :all]
9 | [webdriver.util :refer :all]
10 | [webdriver.form :refer :all])
11 | (:import java.io.File
12 | [org.openqa.selenium TimeoutException NoAlertPresentException WebDriver]))
13 |
14 | ;;;;;;;;;;;;;;;;;;;;;;;;
15 | ;;; ;;;
16 | ;;; Test Definitions ;;;
17 | ;;; ;;;
18 | ;;;;;;;;;;;;;;;;;;;;;;;;
19 | (defn browser-basics
20 | [driver]
21 | (is (instance? WebDriver driver))
22 | (is (= *base-url* (current-url driver)))
23 | (is (= "Ministache" (title driver)))
24 | (is (boolean (re-find #"(?i)html>" (page-source driver)))))
25 |
26 | (defn back-forward-should-traverse-browser-history
27 | [driver]
28 | (-> driver
29 | (find-element {:tag :a, :text "example form"})
30 | click)
31 | (wait-until driver (fn [d] (= (str *base-url* "example-form") (current-url d))))
32 | (is (= (str *base-url* "example-form") (current-url driver)))
33 | (back driver)
34 | (is (= *base-url* (current-url driver)))
35 | (forward driver)
36 | (is (= (str *base-url* "example-form") (current-url driver))))
37 |
38 | (defn to-should-open-given-url-in-browser
39 | [driver]
40 | (to driver (str *base-url* "example-form"))
41 | (is (= (str *base-url* "example-form") (current-url driver)))
42 | (is (= "Ministache" (title driver))))
43 |
44 | (defn should-be-able-to-find-element-bys-using-low-level-by-wrappers
45 | [driver]
46 | (-> driver
47 | (find-element {:tag :a, :text "example form"})
48 | click)
49 | (wait-until driver (fn [d] (immortal (find-element-by d (by-id "first_name")))))
50 | (is (= "first_name"
51 | (attribute (find-element-by driver (by-id "first_name")) :id)))
52 | (is (= "home"
53 | (text (find-element-by driver (by-link-text "home")))))
54 | (is (= "example form"
55 | (text (find-element-by driver (by-partial-link-text "example")))))
56 | (is (= "first_name"
57 | (attribute (find-element-by driver (by-name "first_name")) :id)))
58 | (is (= "home"
59 | (text (find-element-by driver (by-tag "a")))))
60 | (is (= "home"
61 | (text (find-element-by driver (by-xpath "//a[text()='home']")))))
62 | (is (= "home"
63 | (text (find-element-by driver (by-class-name "menu-item")))))
64 | (is (= "home"
65 | (text (find-element-by driver (by-css-selector "#footer a.menu-item")))))
66 | (is (= "social_media"
67 | (attribute (find-element-by driver (by-attr-contains :option :value "cial_")) :value)))
68 | (is (= "social_media"
69 | (attribute (find-element-by driver (by-attr-starts :option :value "social_")) :value)))
70 | (is (= "social_media"
71 | (attribute (find-element-by driver (by-attr-ends :option :value "_media")) :value)))
72 | (is (= "france"
73 | (attribute (find-element-by driver (by-has-attr :option :value)) :value)))
74 | (to driver *base-url*)
75 | (is (= "first odd"
76 | (attribute (find-element-by driver (by-class-name "first odd")) :class))))
77 |
78 | (defn find-element-should-support-basic-attr-val-map
79 | [driver]
80 | (is (= "Moustache"
81 | (text (nth (find-elements driver {:tag :a}) 1))))
82 | (is (= "Moustache"
83 | (text (find-element driver {:class "external"}))))
84 | (is (= "first odd"
85 | (attribute (find-element driver {:class "first odd"}) :class)))
86 | (is (= "first odd"
87 | (attribute (find-element driver {:tag :li, :class "first odd"}) :class)))
88 | (is (= "https://github.com/cgrand/moustache"
89 | (attribute (find-element driver {:text "Moustache"}) "href")))
90 | (is (= 10
91 | (count (find-elements driver {:tag :a}))))
92 | (-> driver
93 | (find-element {:tag :a, :text "example form"})
94 | click)
95 | (wait-until driver (fn [d] (immortal (find-element d {:type "text"}))))
96 | (is (= "first_name"
97 | (attribute (find-element driver {:type "text"}) "id")))
98 | (is (= "first_name"
99 | (attribute (find-element driver {:tag :input, :type "text"}) "id")))
100 | (is (= "first_name"
101 | (attribute (find-element driver {:tag :input, :type "text", :name "first_name"}) "id"))))
102 |
103 | (defn find-element-should-support-hierarchical-querying
104 | [driver]
105 | (is (= "Moustache"
106 | (text (find-element driver [{:tag :div, :id "content"}, {:tag :a, :class "external"}]))))
107 | (is (= "home"
108 | (text (find-element driver [{:tag :*, :id "footer"}, {:tag :a}]))))
109 | (is (= 5
110 | (count (find-elements driver [{:tag :*, :id "footer"}, {:tag :a}])))))
111 |
112 | (defn hierarchical-querying-should-not-support-css-or-xpath-attrs
113 | [driver]
114 | (is (thrown? IllegalArgumentException
115 | (find-element driver [{:tag :div, :id "content", :css "div#content"}, {:tag :a, :class "external"}])))
116 | (is (thrown? IllegalArgumentException
117 | (find-element driver [{:tag :div, :id "content", :xpath "//div[@id='content']"}, {:tag :a, :class "external"}]))))
118 |
119 | (defn exists-should-return-truthy-falsey-and-should-not-throw-an-exception
120 | [driver]
121 | (is (-> driver
122 | (find-element {:tag :a})
123 | exists?))
124 | (is (not
125 | (-> driver
126 | (find-element {:tag :area})
127 | exists?))))
128 |
129 | (defn visible-should-return-truthy-falsey-when-visible
130 | [driver]
131 | (is (-> driver
132 | (find-element {:tag :a, :text "Moustache"})
133 | visible?))
134 | (is (not
135 | (-> driver
136 | (find-element {:tag :a, :href "#pages"})
137 | visible?)))
138 | (is (-> driver
139 | (find-element {:tag :a, :text "Moustache"})
140 | displayed?))
141 | (is (not
142 | (-> driver
143 | (find-element {:tag :a, :href "#pages"})
144 | displayed?))))
145 |
146 | (defn present-should-return-truthy-falsey-when-exists-and-visible
147 | [driver]
148 | (is (-> driver
149 | (find-element {:tag :a, :text "Moustache"})
150 | present?))
151 | (is (not
152 | (-> driver
153 | (find-element {:tag :a, :href "#pages"})
154 | present?))))
155 |
156 | (defn drag-and-drop-by-pixels-should-work
157 | [driver]
158 | (-> driver
159 | (find-element {:tag :a, :text "javascript playground"})
160 | click)
161 | ;; Just check to make sure this page still has the element we expect,
162 | ;; since it's an external site
163 | (wait-until driver (fn [d] (immortal (find-element d {:id "draggable"}))))
164 | (is (-> driver
165 | (find-element {:id "draggable"})
166 | present?))
167 | (let [el-to-drag (find-element driver {:id "draggable"})
168 | {o-x :x o-y :y} (location-in-viewport el-to-drag)
169 | {n-x :x n-y :y} (do
170 | (drag-and-drop-by driver el-to-drag {:x 20 :y 20})
171 | (location-in-viewport el-to-drag))
172 | x-diff (Math/abs (- n-x o-x))
173 | y-diff (Math/abs (- n-y o-y))]
174 | (is (= x-diff 20))
175 | (is (= y-diff 20))))
176 |
177 | (defn drag-and-drop-on-elements-should-work
178 | [driver]
179 | (-> driver
180 | (find-element {:tag :a, :text "javascript playground"})
181 | click)
182 | ;; Just check to make sure this page still has the element we expect,
183 | ;; since it's an external site
184 | (wait-until driver (fn [d] (immortal (find-element d {:id "draggable"}))))
185 | (is (-> driver
186 | (find-element {:id "draggable"})
187 | present?))
188 | (is (-> driver
189 | (find-element {:id "droppable"})
190 | present?))
191 | (let [draggable (find-element driver {:id "draggable"})
192 | droppable (find-element driver {:id "droppable"})
193 | {o-x :x o-y :y} (location-in-viewport draggable)
194 | {n-x :x n-y :y} (do
195 | (drag-and-drop driver draggable droppable)
196 | (location-in-viewport draggable))]
197 | (is (or (not= o-x n-x)
198 | (not= o-y n-y)))
199 | (is (re-find #"ui-state-highlight" (attribute droppable :class)))))
200 |
201 | (defn should-be-able-to-determine-if-elements-intersect-each-other
202 | [driver]
203 | (click (find-element driver {:tag :a, :text "example form"}))
204 | (wait-until driver (fn [d] (immortal (find-element d {:id "first_name"}))))
205 | (is (intersects? (find-element driver {:id "first_name"})
206 | (find-element driver {:id "personal-info-wrapper"})))
207 | (is (not
208 | (intersects? (find-element driver {:id "first_name"})
209 | (find-element driver {:id "last_name"})))))
210 |
211 | ;; Default wrap for strings is double quotes
212 | (defn generated-xpath-should-wrap-strings-in-double-quotes
213 | [driver]
214 | (is (find-element driver {:text "File's Name"})))
215 |
216 | (defn xpath-function-should-return-string-xpath-of-element
217 | [driver]
218 | (is (= (xpath (find-element driver {:tag :a, :text "Moustache"})) "/html/body/div[2]/div/p/a")))
219 |
220 | (defn html-function-should-return-string-html-of-element
221 | [driver]
222 | (is (re-find #"href=\"https://github\.com/cgrand/moustache\"" (html (find-element driver {:tag :a, :text "Moustache"})))))
223 |
224 | (defn find-table-cell-should-find-cell-with-coords
225 | [driver]
226 | (is (= "th"
227 | (lower-case (tag (find-table-cell driver
228 | (find-element driver {:id "pages-table"})
229 | [0 0])))))
230 | (is (= "th"
231 | (lower-case (tag (find-table-cell driver
232 | (find-element driver {:id "pages-table"})
233 | [0 1])))))
234 | (is (= "td"
235 | (lower-case (tag (find-table-cell driver
236 | (find-element driver {:id "pages-table"})
237 | [1 0])))))
238 | (is (= "td"
239 | (lower-case (tag (find-table-cell driver
240 | (find-element driver {:id "pages-table"})
241 | [1 1]))))))
242 |
243 | (defn find-table-row-should-find-all-cells-for-row
244 | [driver]
245 | (is (= 2
246 | (count (find-table-row driver
247 | (find-element driver {:id "pages-table"})
248 | 0))))
249 | (is (= "th"
250 | (lower-case (tag (first (find-table-row driver
251 | (find-element driver {:id "pages-table"})
252 | 0))))))
253 | (is (= "td"
254 | (lower-case (tag (first (find-table-row driver
255 | (find-element driver {:id "pages-table"})
256 | 1)))))))
257 |
258 | (defn form-elements
259 | [driver]
260 | (to driver (str *base-url* "example-form"))
261 | ;; Clear element
262 | ;; (-> driver
263 | ;; (find-element [{:tag :form, :id "example_form"}, {:tag :input, :name #"last_"}])
264 | ;; clear)
265 | ;; (is (= ""
266 | ;; (value (find-element driver [{:tag :form, :id "example_form"}, {:tag :input, :name #"last_"}]))))
267 | ;; Radio buttons
268 | (is (= true
269 | (selected? (find-element driver {:tag :input, :type "radio", :value "male"}))))
270 | (-> driver
271 | (find-element {:tag :input, :type "radio", :value "female"})
272 | select)
273 | (is (= true
274 | (selected? (find-element driver {:tag :input, :type "radio", :value "female"}))))
275 | (-> driver
276 | (find-element {:tag :radio, :value "male"})
277 | select)
278 | (is (= true
279 | (selected? (find-element driver {:tag :input, :type "radio", :value "male"}))))
280 | ;; Checkboxes
281 | ;; (is (= false
282 | ;; (selected? (find-element driver {:tag :input, :type "checkbox", :name #"(?i)clojure"}))))
283 | ;; (-> driver
284 | ;; (find-element {:tag :input, :type "checkbox", :name #"(?i)clojure"})
285 | ;; toggle)
286 | ;; (is (= true
287 | ;; (selected? (find-element driver {:tag :input, :type "checkbox", :name #"(?i)clojure"}))))
288 | ;; (-> driver
289 | ;; (find-element {:tag :checkbox, :name #"(?i)clojure"})
290 | ;; click)
291 | ;; (is (= false
292 | ;; (selected? (find-element driver {:tag :input, :type "checkbox", :name #"(?i)clojure"}))))
293 | ;; (-> driver
294 | ;; (find-element {:tag :checkbox, :type "checkbox", :name #"(?i)clojure"})
295 | ;; select)
296 | ;; (is (= true
297 | ;; (selected? (find-element driver {:tag :input, :type "checkbox", :name #"(?i)clojure"}))))
298 | ;; Text fields
299 | (println (current-url driver))
300 | (-> driver
301 | (find-element {:tag :input, :id "first_name"})
302 | (input-text "foobar"))
303 | (is (= "foobar"
304 | (value (find-element driver {:tag :input, :id "first_name"}))))
305 | (-> driver
306 | (find-element {:tag :textfield, :id "first_name"})
307 | clear
308 | (input-text "clojurian"))
309 | (is (= "clojurian"
310 | (value (find-element driver {:tag :textfield, :id "first_name"}))))
311 | ;; Boolean attributes (disabled, readonly, etc)
312 | (is (= "disabled"
313 | (attribute (find-element driver {:id "disabled_field"}) :disabled)))
314 | (is (= "readonly"
315 | (attribute (find-element driver {:id "purpose_here"}) :readonly)))
316 | (is (nil?
317 | (attribute (find-element driver {:id "disabled_field"}) :readonly)))
318 | (is (nil?
319 | (attribute (find-element driver {:id "purpose_here"}) :disabled)))
320 | ;; Buttons
321 | ;; (is (= 4
322 | ;; (count (find-elements driver {:tag :button*}))))
323 | ;; (is (= 1
324 | ;; (count (find-elements driver {:tag :button*, :class "button-button"}))))
325 | ;; (is (= 1
326 | ;; (count (find-elements driver {:tag :button*, :id "input-input-button"}))))
327 | ;; (is (= 1
328 | ;; (count (find-elements driver {:tag :button*, :class "input-submit-button"}))))
329 | ;; (is (= 1
330 | ;; (count (find-elements driver {:tag :button*, :class "input-reset-button"}))))
331 | )
332 |
333 | (defn select-element-functions-should-behave-as-expected
334 | [driver]
335 | (to driver (str *base-url* "example-form"))
336 | (let [select-el (find-element driver {:tag "select", :id "countries"})]
337 | (is (= 4
338 | (count (all-options select-el))))
339 | (is (= 1
340 | (count (all-selected-options select-el))))
341 | (is (= "bharat"
342 | (attribute (first-selected-option select-el) :value)))
343 | (is (= "bharat"
344 | (attribute (first (all-selected-options select-el)) :value)))
345 | (is (false?
346 | (multiple? select-el)))
347 | (select-option select-el
348 | {:value "deutschland"})
349 | (is (= 1
350 | (count (all-selected-options select-el))))
351 | (is (= "deutschland"
352 | (attribute (first-selected-option select-el) :value)))
353 | (is (= "deutschland"
354 | (attribute (first (all-selected-options select-el)) :value)))
355 | (select-by-index select-el
356 | 0)
357 | (is (= 1
358 | (count (all-selected-options select-el))))
359 | (is (= "france"
360 | (attribute (first-selected-option select-el) :value)))
361 | (is (= "france"
362 | (attribute (first (all-selected-options select-el)) :value)))
363 | (select-by-text select-el
364 | "Haiti")
365 | (is (= 1
366 | (count (all-selected-options select-el))))
367 | (is (= "ayiti"
368 | (attribute (first-selected-option select-el) :value)))
369 | (is (= "ayiti"
370 | (attribute (first (all-selected-options select-el)) :value)))
371 | (select-by-value select-el
372 | "bharat")
373 | (is (= 1
374 | (count (all-selected-options select-el))))
375 | (is (= "bharat"
376 | (attribute (first-selected-option select-el) :value)))
377 | (is (= "bharat"
378 | (attribute (first (all-selected-options select-el)) :value))))
379 | (let [select-el (find-element driver {:tag "select", :id "site_types"})]
380 | (is (true?
381 | (multiple? select-el)))
382 | (is (= 4
383 | (count (all-options select-el))))
384 | (is (zero?
385 | (count (all-selected-options select-el))))
386 | (select-option select-el {:index 0})
387 | (is (= 1
388 | (count (all-selected-options select-el))))
389 | (is (= "blog"
390 | (attribute (first-selected-option select-el) :value)))
391 | (is (= "blog"
392 | (attribute (first (all-selected-options select-el)) :value)))
393 | (select-option select-el {:value "social_media"})
394 | (is (= 2
395 | (count (all-selected-options select-el))))
396 | (is (= "social_media"
397 | (attribute (second (all-selected-options select-el)) :value)))
398 | (deselect-option select-el {:index 0})
399 | (is (= 1
400 | (count (all-selected-options select-el))))
401 | (is (= "social_media"
402 | (attribute (first-selected-option select-el) :value)))
403 | (is (= "social_media"
404 | (attribute (first (all-selected-options select-el)) :value)))
405 | (select-option select-el {:value "search_engine"})
406 | (is (= 2
407 | (count (all-selected-options select-el))))
408 | (is (= "search_engine"
409 | (attribute (second (all-selected-options select-el)) :value)))
410 | (deselect-by-index select-el 1)
411 | (is (= 1
412 | (count (all-selected-options select-el))))
413 | (is (= "search_engine"
414 | (attribute (first-selected-option select-el) :value)))
415 | (is (= "search_engine"
416 | (attribute (first (all-selected-options select-el)) :value)))
417 | (select-option select-el {:value "code"})
418 | (is (= 2
419 | (count (all-selected-options select-el))))
420 | (is (= "code"
421 | (attribute (last (all-selected-options select-el)) :value)))
422 | (deselect-by-text select-el "Search Engine")
423 | (is (= 1
424 | (count (all-selected-options select-el))))
425 | (is (= "code"
426 | (attribute (first-selected-option select-el) :value)))
427 | (select-all select-el)
428 | (is (= 4
429 | (count (all-selected-options select-el))))
430 | (deselect-all select-el)
431 | (is (zero?
432 | (count (all-selected-options select-el))))))
433 |
434 | (defn quick-fill-should-accept-special-seq-and-perform-batch-actions-on-form
435 | [driver]
436 | (to driver (str *base-url* "example-form"))
437 | (quick-fill driver
438 | [{"first_name" clear}
439 | {"first_name" "Richard"}
440 | {{:id "last_name"} clear}
441 | {{:id "last_name"} "Hickey"}
442 | {{:name "bio"} clear}
443 | {{:name "bio"} #(input-text % "Creator of Clojure")}
444 | {{:tag "input", :type "radio", :value "female"} click}
445 | {{:css "select#countries"} #(select-by-value % "france")}])
446 | (is (= "Richard"
447 | (value (find-element driver {:tag :input, :id "first_name"}))))
448 | (is (= "Hickey"
449 | (value (find-element driver {:tag :input, :id "last_name"}))))
450 | (is (= "Creator of Clojure"
451 | (value (find-element driver {:tag :textarea, :name "bio"}))))
452 | (is (selected?
453 | (find-element driver {:tag :input, :type "radio", :value "female"})))
454 | (is (selected?
455 | (find-element driver {:tag :option, :value "france"}))))
456 |
457 | (defn quick-fill-submit-should-always-return-nil
458 | [driver]
459 | (to driver (str *base-url* "example-form"))
460 | (is (nil?
461 | (quick-fill-submit driver
462 | [{"first_name" clear}
463 | {"first_name" "Richard"}
464 | {{:id "last_name"} clear}
465 | {{:id "last_name"} "Hickey"}
466 | {{:name "bio"} clear}
467 | {{:name "bio"} #(input-text % "Creator of Clojure")}
468 | {{:tag "input", :type "radio", :value "female"} click}
469 | {{:css "select#countries"} #(select-by-value % "france")}]))))
470 |
471 | (defn should-be-able-to-toggle-between-open-windows
472 | [driver]
473 | (let [window-1 (window-handle driver)]
474 | (is (= (count (window-handles driver))
475 | 1))
476 | (-> driver
477 | (find-element {:tag :a, :text "is amazing!"})
478 | click)
479 | (wait-until driver (fn [d] (immortal (= "Ministache" (title d)))))
480 | (let [window-2 (first (disj (window-handles driver) window-1))]
481 | (is (= (count (window-handles driver))
482 | 2))
483 | (is (not= window-1 window-2))
484 | (is (= (title driver)
485 | "Ministache"))
486 | (switch-to-window driver window-2)
487 | (is (= (str *base-url* "clojure")
488 | (current-url driver)))
489 | (switch-to-other-window driver)
490 | (is (= *base-url* (current-url driver)))
491 | (switch-to-other-window driver)
492 | (close driver)
493 | (switch-to-window driver (first (window-handles driver)))
494 | (= *base-url* (current-url driver)))))
495 |
496 | (defn alert-dialog-handling
497 | [driver]
498 | (click (find-element driver {:text "example form"}))
499 | (wait-until driver (fn [d] (immortal (find-element d {:tag :button}))))
500 | (let [act (fn [] (click (find-element driver {:tag :button})))]
501 | (act)
502 | (is (alert-obj driver) "No alert dialog could be located")
503 | (accept driver)
504 | (is (thrown? NoAlertPresentException
505 | (alert-obj driver)))
506 | (act)
507 | (is (= (alert-text driver)
508 | "Testing alerts."))
509 | (dismiss driver)
510 | (is (thrown? NoAlertPresentException
511 | (alert-obj driver)))))
512 |
513 | (defn wait-until-should-wait-for-condition
514 | [driver]
515 | (is (= "Ministache" (title driver)))
516 | (execute-script driver "setTimeout(function () { window.document.title = \"asdf\"}, 2000)")
517 | (wait-until driver (fn [d] (= (title d) "asdf")))
518 | (is (= (title driver) "asdf")))
519 |
520 | (defn wait-until-should-throw-on-timeout
521 | [driver]
522 | (is (thrown? TimeoutException
523 | (do
524 | (execute-script driver "setTimeout(function () { window.document.title = \"test\"}, 6000)")
525 | (wait-until driver (fn [d] (= "test" (title d))))))))
526 |
527 | (defn wait-until-should-allow-timeout-argument
528 | [driver]
529 | (is (thrown? TimeoutException
530 | (do
531 | (execute-script driver "setTimeout(function () { window.document.title = \"test\"}, 10000)")
532 | (wait-until driver (fn [d#] (= (title d#) "test")) 1000)))))
533 |
534 | (defn implicit-wait-should-cause-find-to-wait
535 | [driver]
536 | (implicit-wait driver 3000)
537 | (execute-script driver "setTimeout(function () { window.document.body.innerHTML = \"hi!
\"}, 1000)")
538 | (is (= "test"
539 | (attribute (find-element-by driver (by-id "test")) :id))))
540 |
541 | ;; This behavior is so inconsistent, I'm almost inclined to take it out
542 | ;; of clj-webdriver entirely.
543 | ;;
544 | ;; (defn frames-by-index
545 | ;; [driver]
546 | ;; (to driver "http://selenium.googlecode.com/svn/trunk/docs/api/java/index.html")
547 | ;; (is (= (count (find-elements driver {:tag :frame})) 3))
548 | ;; (switch-to-frame driver 0)
549 | ;; (is (exclusive-between (count (find-elements driver {:tag :a}))
550 | ;; 50 100))
551 | ;; (switch-to-default driver)
552 | ;; (switch-to-frame driver 1)
553 | ;; (is (exclusive-between (count (find-elements driver {:tag :a}))
554 | ;; 370 400))
555 | ;; (switch-to-default driver)
556 | ;; (switch-to-frame driver 2)
557 | ;; (is (exclusive-between (count (find-elements driver {:tag :a}))
558 | ;; 30 50)))
559 |
560 | (defn frames-by-element
561 | [driver]
562 | ;; TODO Implement page with frames
563 | (to driver "http://selenium.googlecode.com/svn/trunk/docs/api/java/index.html")
564 | (is (= (count (find-elements driver {:tag :frame})) 3))
565 | (switch-to-frame driver (find-element driver {:name "packageListFrame"}))
566 | (is (exclusive-between (count (find-elements driver {:tag :a}))
567 | 30 50))
568 | (switch-to-default driver)
569 | (switch-to-frame driver (find-element driver {:name "packageFrame"}))
570 | (is (exclusive-between (count (find-elements driver {:tag :a}))
571 | 370 400))
572 | (switch-to-default driver)
573 | (switch-to-frame driver (find-element driver {:name "classFrame"}))
574 | (is (exclusive-between (count (find-elements driver {:tag :a}))
575 | 50 100)))
576 |
577 | ;; Not sure how we'll test that flash in fact flashes,
578 | ;; but at least this exercises the function call.
579 | (defn flash-helper
580 | [driver]
581 | (-> driver
582 | (find-element {:tag :a, :text "Moustache"})
583 | flash))
584 |
585 | (def tmpdir (let [td (System/getProperty "java.io.tmpdir")]
586 | (if (.endsWith td File/separator)
587 | td
588 | (str td File/separator))))
589 | (def screenshot-file (str tmpdir "screenshot_test.png"))
590 | (defn take-screenshot
591 | [driver]
592 | (is (string? (get-screenshot driver :base64)))
593 | (is (> (count (get-screenshot driver :bytes)) 0))
594 | (is (= (class (get-screenshot driver :file)) java.io.File))
595 | (is (= (class (get-screenshot driver :file screenshot-file)) java.io.File))
596 | ;; the following will throw an exception if deletion fails, hence our test
597 | (io/delete-file screenshot-file))
598 |
599 | ;;; Fixture fn's ;;;
600 | (defn reset-driver
601 | [driver]
602 | (to driver *base-url*))
603 |
604 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
605 | ;;; ;;;
606 | ;;; RUN ACTUAL TESTS HERE ;;;
607 | ;;; ;;;
608 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
609 | (def common-tests [#'browser-basics
610 | #'back-forward-should-traverse-browser-history
611 | #'to-should-open-given-url-in-browser
612 | #'should-be-able-to-find-element-bys-using-low-level-by-wrappers
613 | #'find-element-should-support-basic-attr-val-map
614 | #'find-element-should-support-hierarchical-querying
615 | #'hierarchical-querying-should-not-support-css-or-xpath-attrs
616 | #'exists-should-return-truthy-falsey-and-should-not-throw-an-exception
617 | #'visible-should-return-truthy-falsey-when-visible
618 | #'present-should-return-truthy-falsey-when-exists-and-visible
619 | #'drag-and-drop-by-pixels-should-work
620 | #'drag-and-drop-on-elements-should-work
621 | #'should-be-able-to-determine-if-elements-intersect-each-other
622 | #'generated-xpath-should-wrap-strings-in-double-quotes
623 | #'xpath-function-should-return-string-xpath-of-element
624 | #'html-function-should-return-string-html-of-element
625 | #'find-table-cell-should-find-cell-with-coords
626 | #'find-table-row-should-find-all-cells-for-row
627 | #'form-elements
628 | #'select-element-functions-should-behave-as-expected
629 | #'quick-fill-should-accept-special-seq-and-perform-batch-actions-on-form
630 | #'quick-fill-submit-should-always-return-nil
631 | #'should-be-able-to-toggle-between-open-windows
632 | #'wait-until-should-wait-for-condition
633 | #'wait-until-should-throw-on-timeout
634 | #'wait-until-should-allow-timeout-argument
635 | #'implicit-wait-should-cause-find-to-wait
636 | #'frames-by-element
637 | #'flash-helper
638 | #'take-screenshot])
639 |
640 | (defn defcommontests*
641 | ([prefix driver] (defcommontests* prefix webdriver.test.helpers/*base-url* driver))
642 | ([prefix url driver]
643 | (let [test-names (mapv #(symbol (str prefix (:name (meta %)))) common-tests)
644 | ts (mapv (fn [v n]
645 | `(deftest ~n
646 | (binding [webdriver.test.helpers/*base-url* ~url]
647 | (~v ~driver))))
648 | common-tests test-names)]
649 | `(do
650 | ~@ts))))
651 |
652 | (defmacro defcommontests
653 | "This can be run from within another namespace to have this suite defined and run there. This gives a separate deftest for each test, which makes it much easier to understand what broke, and run individual tests within those namespaces more easily."
654 | [& args]
655 | (apply defcommontests* args))
656 |
657 | (def alert-tests [#'alert-dialog-handling])
658 |
--------------------------------------------------------------------------------