├── .gitignore ├── .travis.yml ├── README.md ├── project.clj ├── resources ├── admin.html ├── clojure.html ├── form.html ├── javascript.html ├── log4j.properties ├── page.html ├── public │ └── js │ │ └── jquery │ │ ├── jquery-1.8.2.min.js │ │ └── jquery-ui-1.8.23.min.js └── welcome.html ├── script ├── grid-hub ├── grid-node ├── release └── util ├── src └── webdriver │ ├── core.clj │ ├── core_actions.clj │ ├── core_by.clj │ ├── core_driver.clj │ ├── core_element.clj │ ├── core_wait.clj │ ├── core_window.clj │ ├── firefox.clj │ ├── form.clj │ ├── js │ └── browserbot.clj │ └── util.clj └── test └── webdriver ├── chrome_test.clj ├── firefox_test.clj ├── phantomjs_test.clj ├── saucelabs_test.clj ├── test ├── common.clj ├── example_app.clj └── helpers.clj ├── util_test.clj └── window_test.clj /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/admin.html: -------------------------------------------------------------------------------- 1 |
You made it! The username and password were quite difficult to guess, weren't they?
4 | -------------------------------------------------------------------------------- /resources/clojure.html: -------------------------------------------------------------------------------- 1 |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 | -------------------------------------------------------------------------------- /resources/form.html: -------------------------------------------------------------------------------- 1 |
draggable
6 |droppable
9 |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 |File's Name | 17 |Purpose of "File" | 18 |
---|---|
welcome.html (row 1, cell 1) | 23 |Introduction, test generic HTML elements (row 1, cell 2) | 24 |
form.html (row 2, cell 1) | 27 |Test HTML form elements (row 2, cell 2) | 28 |
clojure.html (row 3, cell 1) | 31 |Test window handling (row 3, cell 2) | 32 |
By the way, Clojure is amazing! for the following reasons:
36 | 37 |Enjoy! See the links below.
50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 = \"