diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index cc6173cb..1318d22f 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -14,7 +14,7 @@ The symbol `num-.` is technically an invalid Clojure symbol and can confuse tool A grep.app for `num-.` found Etaoin itself as the only user of this var. If your code uses `etaoin.keys/num-.`, you'll need to rename it to `etaoin.keys/num-dot`. * https://github.com/clj-commons/etaoin/issues/430[#430]: Declare the public API. -We made what we think is a good guess at what the public etaoin API is. +We made what we think is a good guess at what the public Etaoin API is. The following namespaces are now considered internal and subject to change: + [%autowidth] @@ -43,6 +43,48 @@ The following namespaces are now considered internal and subject to change: | `etaoin.ide.impl.spec` |=== ++ +The following vars are now considered internal and subject to change: ++ +[%autowidth] +|=== +| namespace | var + +.16+|`etaoin.api` +| `default-locator` +| `dispatch-driver` +| `find-elements*` +| `format-date` +| `get-pwd` +| `join-path` +| `locator-css` +| `locator-xpath` +| `make-url` +| `make-screenshot-file-path` +| `postmortem-handler` +| `process-log` +| `proxy-env` +| `use-locator` +| `with-exception` +| `with-locator` + +.8+| `etaoin.dev` +| `build-request` +| `group-requests` +| `log->request` +| `parse-json` +| `parse-method` +| `process-log` +| `request?` +| `try-parse-int` + +| `etaoin.ide.flow` +| all except for: `run-ide-script` + +| `etaoin.ide.main` +| all except for: `-main` +|=== ++ If we got this wrong your code will fail, you will tell us, and we can discuss. Other Changes @@ -63,8 +105,10 @@ Fixed. * https://github.com/clj-commons/etaoin/issues/444[#444]: Visibility checks fixed for firefox and chrome (thanks https://github.com/daveyarwood[@daveyarwood]!) * https://github.com/clj-commons/etaoin/issues/446[#446]: Bump Etaoin dependencies to current releases * Docs +** Reviewed and updated API docstrings ** https://github.com/clj-commons/etaoin/issues/393[#393]: Add changelog ** https://github.com/clj-commons/etaoin/issues/426[#426]: Reorganize into separate guides + ** https://github.com/clj-commons/etaoin/issues/396[#396]: Move from Markdown to AsciiDoc ** User guide *** Reviewed, re-organized, hopefully clarified some things diff --git a/README.adoc b/README.adoc index a1be86cd..7144375a 100644 --- a/README.adoc +++ b/README.adoc @@ -14,23 +14,23 @@ https://clojars.org/{project-mvn-coords}[image:https://img.shields.io/clojars/v/ https://babashka.org[image:https://raw.githubusercontent.com/babashka/babashka/master/logo/badge.svg[bb compatible]] https://clojurians.slack.com/archives/C7KDM0EKW[image:https://img.shields.io/badge/slack-join_chat-brightgreen.svg[Join chat]] -A pure Clojure implementation of the link:{url-webdriver}[Webdriver] protocol named after link:{url-wiki}[Etaoin Shrdlu] -- a typing machine that came to life after a mysterious note was produced on it. +A pure Clojure implementation of the link:{url-webdriver}[Webdriver] protocol, named after link:{url-wiki}[Etaoin Shrdlu] -- a typing machine that came to life after a mysterious note was produced on it. Use the Etaoin library to automate a browser, test your frontend behaviour, simulate human actions or whatever you want. == Benefits -* Selenium-free: no long dependencies, no tons of downloaded jars, etc. +* Selenium-free: no big dependencies, no tons of downloaded jars, etc. * Lightweight, fast. Simple, easy to understand. -* Compact: just one main module with a couple of helpers. +* Compact: just one main namespace with a couple of helpers. * Declarative: the code is just a list of actions. == Capabilities * Currently supports Chrome, Firefox, Safari and Edge. -* May either connect to a remote driver or run it on your local machine. -* Run your unit tests directly from Emacs pressing `C-t t` as usual. +* Can either connect to a remote WebDriver process, or have Etaoin launch one for you. +* Run your unit tests directly from Emacs by pressing `C-t t` as usual. * Can imitate human-like behaviour (delays, typos, etc). == Documentation @@ -54,13 +54,12 @@ Ivan's blog-post about pitfalls that can occur when testing UI. * https://www.exoscale.com/[Exoscale] * https://www.flyerbee.com/[Flyerbee] -* https://www.roomkey.com/[Roomkey] * http://www.barrick.com/[Barrick Gold] * http://drevidence.com/[Doctor Evidence] * https://kevel.com/[Kevel (formerly Adzerk)] * https://www.rate.com/[Guaranteed Rate] -You are welcome to submit your company to this list. +You are most welcome to submit your company or project to this list. == Versioning @@ -95,8 +94,8 @@ Can be `alpha`, `beta`, `rc1`, etc. * https://github.com/igrishaev[Ivan Grishaev] -The project is open for your improvements and ideas. -If any of unit tests fail on your machine please submit an issue giving your OS version, browser and console output. +Etaoin is open for your improvements and ideas. +If any of unit tests fail on your machine, please submit an issue giving your OS version, browser and console output. == License diff --git a/deps.edn b/deps.edn index d6398de6..f5d82342 100644 --- a/deps.edn +++ b/deps.edn @@ -16,7 +16,7 @@ org.slf4j/jcl-over-slf4j {:mvn/version "2.0.0-alpha7"}} :main-opts ["-m" "cognitect.test-runner"]} :script {:extra-paths ["script"]} - ;; for babashka testing, needed for eatoin.ide + ;; for babashka testing, needed for etaoin.ide :bb-spec {:extra-deps {org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha" :sha "8df0712896f596680da7a32ae44bb000b7e45e68"}}} ;; for babashka testing, allows us to use cognitect test-runner diff --git a/doc/01-user-guide.adoc b/doc/01-user-guide.adoc index 6a2ab69c..0163e9e0 100644 --- a/doc/01-user-guide.adoc +++ b/doc/01-user-guide.adoc @@ -59,7 +59,7 @@ Etaoin's test suite covers the following OSes and browsers for both Clojure and |=== -NOTE: We once did test against PhantomJS, but since work has long ago stopped on this project, we have dropped testing +NOTE: We did once test against PhantomJS, but since work has long ago stopped on this project, we have dropped testing == Installation @@ -68,7 +68,7 @@ There are two steps to installation: . Add the `etaoin` library as a dependency to your project . Install the WebDriver for each web browser that you want to control with Etaoin -=== Installing the Etaoin Library +=== Add the Etaoin Library Dependency ==== For Clojure Users @@ -420,7 +420,7 @@ The v2 API has ergonomic `with-` functions that handle cleanup nicely: Replace `chrome` with `firefox`, `edge` or `safari` for other variants. See link:{url-doc}[API docs] for details. -See <> for all options available when creating a driver. +See <> for all options available when creating a driver. == Selecting Elements [[querying]] @@ -747,7 +747,7 @@ Notice: * The nth offset of 1 instead of 2. Clojure's nth is 0-based, and our search indexes are 1-based. ==== -==== Querying a Tree +==== Querying a Tree [[query-tree]] `query-tree` pipes selectors. Every selector queries elements from the previous one. @@ -1313,7 +1313,7 @@ Load strategy option of `:none`: The `:eager` option only works with Firefox at the moment. -=== Actions +=== Actions [[actions]] Etaoin supports link:{actions}[Webdriver Actions]. They are described as "virtual input devices". @@ -1514,7 +1514,7 @@ The message text and the source type will vary by browser vendor. Chrome wipes the logs once they have been read. Phantom.js wipes the logs when the page location changes. -=== DevTools: Tracking HTTP Requests, XHR (Ajax) +=== DevTools: Tracking HTTP Requests, XHR (Ajax) [[devtools]] You can trace events that come from the DevTools panel. This means that everything you see in the developer console now is available through the Etaoin API. @@ -1899,9 +1899,9 @@ The available `with-postmortem` options are: :date-format "yyyy-MM-dd-HH-mm-ss"} ---- -== Additional Driver Parameters [[parameters]] +== Driver Options [[driver-options]] -When creating a driver instance, a map of additional parameters can be passed to tweak the WebDriver and web browser behaviour. +When creating a driver instance, a map of additional parameters can optionally be passed to tweak the WebDriver and web browser behaviour. Here, for example, we set an explicit path to the chrome WebDriver binary: @@ -2236,7 +2236,7 @@ To connect to an existing WebDriver, specify the `:host` parameter. TIP: When the `:host` parameter is not specified Etaoin will create a new WebDriver process. The `:host` can be a hostname (localhost, some.remote.host.net) or an IP address (127.0.0.1, 183.102.156.31). -If the port is not specified, the <> `:port` is assumed. +If the port is not specified, the <> `:port` is assumed. Example: @@ -2553,8 +2553,7 @@ Example: (is found (format "No *.xlsx file found in %s directory." DL-DIR))) ---- -[[selenium-ide]] -== Running Selenium IDE files +== Running Selenium IDE files [[selenium-ide]] Etaoin can play the files produced by link:{ide}[Selenium IDE]. Selenium IDE allows you to record web interactions for later playback. @@ -2605,7 +2604,7 @@ Now that you have a `test.side` file, you could do something like this: Everything related to the IDE feature can be found under the link:{url-doc}/CURRENT/api/etaoin.ide[etaoin.ide] namespace. -=== CLI Arguments +=== CLI Arguments [[selenium-ide-cli]] You may also run a `.side` script from the command line. Here is a `clojure` example: @@ -2668,7 +2667,7 @@ docker run --name geckodriver -p 4444:4444 -d instrumentisto/geckodriver To connect to an existing running WebDriver process you need to specify the `:host`. In this example `:host` would be `localhost` or `127.0.0.1`. The `:port` would be the appropirate port for the running WebDriver process as exposed by docker. -If the port is not specified, the <> port is set. +If the port is not specified, the <> port is set. //:test-doc-blocks/skip [source,clojure] diff --git a/doc/02-developer-guide.adoc b/doc/02-developer-guide.adoc index e10a956a..132e3139 100644 --- a/doc/02-developer-guide.adoc +++ b/doc/02-developer-guide.adoc @@ -257,3 +257,9 @@ Etaoin contains a fair number of macros. Clj-kondo can need special configuration (including hooks) to understand the effects of these macros. So, when adding any new macros, think also about our Etaoin users and our clj-kondo export configuration. ==== + +== Useful References + +* https://chromium.googlesource.com/chromium/src/+/master/chrome/test/chromedriver/[chromedriver] +* https://github.com/mozilla/geckodriver[firefox geckodriver], https://searchfox.org/mozilla-central/source/testing/webdriver[sources] +* https://github.com/detro/ghostdriver/blob/[Phantom.js (obsolete, no longer tested)] diff --git a/src/etaoin/api.clj b/src/etaoin/api.clj index 8bd39700..1f154755 100644 --- a/src/etaoin/api.clj +++ b/src/etaoin/api.clj @@ -1,21 +1,144 @@ (ns etaoin.api - " - The API below was written regarding to the source code - of different Webdriver implementations. All of them partially - differ from the official W3C specification. - - The standard: - https://www.w3.org/TR/webdriver/ - - Chrome: - https://chromium.googlesource.com/chromium/src/+/master/chrome/test/chromedriver/ - - Firefox (Geckodriver): - https://github.com/mozilla/geckodriver - https://github.com/mozilla/webdriver-rust/ - - Phantom.js (Ghostdriver) - obsolete and no longer tested - https://github.com/detro/ghostdriver/blob/ + "A wrapper over the [W3C WebDriver Specification](https://www.w3.org/TR/webdriver/) to automate popular browsers. + + Tries to normalize differences across the various implementations. + + See the [User Guide](/doc/01-user-guide.adoc) for details and examples. + + This is a rich API: + + **WebDriver** + - [[with-driver]] [[boot-driver]] [[defaults]] [[when-not-drivers]] + - [[with-chrome]] [[with-chrome-headless]] [[chrome]] [[chrome-headless]] [[chrome?]] [[when-chrome]] [[when-not-chrome]] + - [[with-edge]] [[with-edge-headless]] [[edge]] [[edge-headless]] [[when-edge]] [[when-not-edge]] + - [[with-firefox]] [[with-firefox-headless]] [[firefox]] [[firefox-headless]] [[firefox?]] [[when-firefox]] [[when-not-firefox]] + - [[with-phantom]] [[phantom]] [[phantom?]] [[when-phantom]] [[when-not-phantom]] + - [[with-safari]] [[safari]] [[safari?]] [[when-safari]] [[when-not-safari]] + - [[driver?]] [[running?]] [[headless?]] [[when-headless]] [[when-not-headless]] + - [[disconnect-driver]] [[stop-driver]] [[quit]] + - see also [[etaoin.api2]] + + **WebDriver Lower Level Comms** + - [[execute]] [[with-http-error]] + + **Driver Sessions** + - [[get-status]] [[create-session]] [[delete-session]] + + **Querying/Selecting DOM Elements** + - [[query]] [[query-all]] [[query-tree]] + - [[exists?]] [[absent?]] + - [[displayed?]] [[displayed-el?]] [[enabled?]] [[enabled-el?]] [[disabled?]] [[invisible?]] [[visible?]] + - [[child]] [[children]] + - [[get-element-tag]] [[get-element-tag-el]] + - [[get-element-attr]] [[get-element-attr-el]] [[get-element-attrs]] + - [[get-element-property]] [[get-element-properties]] + - [[has-class?]] [[has-class-el?]] [[has-no-class?]] + - [[get-element-css]] [[get-element-csss]] + - [[get-element-text]] [[get-element-text-el]] [[has-text?]] + - [[get-element-inner-html]] [[get-element-inner-html-el]] + - [[get-element-value]] [[get-element-value-el]] + - [[get-element-size]] [[get-element-size-el]] [[get-element-location]] [[get-element-location-el]] [[get-element-box]] [[intersects?]] + - [[use-css]] [[with-css]] [[use-xpath]] [[with-xpath]] + + **Browser Navigation** + - [[go]] [[get-url]] [[get-hash]] [[set-hash]] + - [[back]] [[forward]] + - [[refresh]] [[reload]] + + **Mouse/Pointer** + - [[click]] [[click-el]] [[click-single]] [[click-multi]] + - [[left-click]] [[left-click-on]] + - [[middle-click]] [[middle-click-on]] + - [[right-click]] [[right-click-on]] + - [[mouse-click]] [[mouse-move-to]] [[mouse-click-on]] + - [[double-click]] [[double-click-el]] + - [[drag-and-drop]] [[mouse-btn-down]] [[mouse-btn-up]] + - [[touch-down]] [[touch-move]] [[touch-tap]] [[touch-up]] + + **Inputs/Forms** + - [[fill]] [[fill-active]] [[fill-el]] [[fill-multi]] + - [[fill-human]] [[fill-human-el]] [[fill-human-multi]] + - [[select]] [[selected?]] [[selected-el?]] + - [[upload-file]] + - [[disabled?]] [[enabled?]] + - [[clear]] [[clear-el]] + - [[submit]] + + **Cookies** + - [[get-cookie]] [[get-cookies]] + - [[set-cookie]] + - [[delete-cookie]] [[delete-cookies]] + + **Alerts** + - [[has-alert?]] [[has-no-alert?]] + - [[get-alert-text]] + - [[accept-alert]] [[dismiss-alert]] + + **Scrolling** + - [[get-scroll]] + - [[scroll]] [[scroll-by]] + - [[scroll-bottom]] [[scroll-top]] + - [[scroll-down]] [[scroll-up]] [[scroll-left]] [[scroll-right]] + - [[scroll-offset]] + - [[scroll-query]] + + **Scripting** + - [[js-execute]] [[js-async]] [[js-localstorage-clear]] [[el->ref]] [[add-script]] + + **Browser Windows** + - [[get-window-handle]] [[get-window-handles]] + - [[get-window-position]] [[set-window-position]] + - [[get-window-size]] [[set-window-size]] + - [[maximize]] + - [[switch-window]] [[switch-window-next]] + - [[close-window]] + + **Frames** + - [[switch-frame]] [[switch-frame-first]] [[switch-frame-parent]] [[switch-frame-top]] [[with-frame]] + + **Page Info** + - [[get-source]] [[get-title]] + + **Screenshots** + - [[screenshot]] [[screenshot-element]] [[with-screenshots]] + + **Browser Info** + - [[supports-logs?]] [[get-log-types]] [[get-logs]] + - [[get-user-agent]] + - [[with-postmortem]] + + **Waiting** + - [[doto-wait]] [[wait]] [[with-wait]] + - [[*wait-interval*]] [[*wait-timeout*]] [[with-wait-interval]] [[with-wait-timeout]] + - [[wait-exists]] [[wait-absent]] + - [[wait-visible]] [[wait-invisible]] + - [[wait-disabled]] [[wait-enabled]] + - [[wait-has-alert]] + - [[wait-has-class]] + - [[wait-has-text]] [[wait-has-text-everywhere]] + - [[wait-predicate]] + - [[wait-running]] + + **Browser Timeouts** + - [[get-implicit-timeout]] [[set-implicit-timeout]] + - [[get-page-load-timeout]] [[set-page-load-timeout]] + - [[get-script-timeout]] [[set-script-timeout]] [[with-script-timeout]] + + **WebDriver Actions** + - [[make-action-input]] [[make-key-input]] [[make-mouse-input]] [[make-pen-input]] [[make-pointer-input]] [[make-touch-input]] + - [[add-action]] + - [[add-double-pause]] + - [[add-key-down]] [[add-key-press]] [[add-key-up]] [[with-key-down]] + - [[add-pause]] + - [[add-pointer-click]] [[add-pointer-click-el]] [[add-pointer-double-click]] [[add-pointer-double-click-el]] + - [[add-pointer-down]] [[add-pointer-up]] + - [[with-pointer-btn-down]] [[with-pointer-left-btn-down]] [[with-mouse-btn]] + - [[add-pointer-move]] [[add-pointer-move-to-el]] + - [[add-pointer-cancel]] + - [[perform-actions]] [[release-actions]] + + **Convenience** + - [[rand-uuid]] [[when-predicate]] [[when-not-predicate]] " (:require [babashka.fs :as fs] @@ -31,16 +154,15 @@ [etaoin.keys :as k] [etaoin.query :as query] [slingshot.slingshot :refer [throw+ try+]]) - (:import java.text.SimpleDateFormat (java.util Base64 Date))) ;; -;; defaults +;; WebDriver defaults ;; -(def defaults +(def ^{:doc "WebDriver defaults"} defaults {:firefox {:port 4444 :path "geckodriver"} :chrome {:port 9515 @@ -52,15 +174,15 @@ :edge {:port 17556 :path "msedgedriver"}}) -(def default-locator "xpath") -(def locator-xpath "xpath") -(def locator-css "css selector") +(def ^:no-doc default-locator "xpath") +(def ^:no-doc locator-xpath "xpath") +(def ^:no-doc locator-css "css selector") ;; ;; utils ;; -(defn dispatch-driver +(defn ^:no-doc dispatch-driver "Returns the current driver's type. Used as dispatcher in multimethods." [driver & _] @@ -76,36 +198,30 @@ ;; (defn execute - "Executes an HTTP request to a driver's server. Performs the body - within result data bound to the `result` clause. - - Arguments: - - - `driver`: a driver instance, - - - `method`: a keyword represents HTTP method, e.g. `:get`, `:post`, - `:delete`, etc. - - - `path`: a vector of strings/keywords represents a server's - path. For example: - - `[:session \"aaaa-bbbb-cccc\" :element \"dddd-eeee\" :find]` - - will turn into \"/session/aaaa-bbbb-cccc/element/dddd-eeee/find\". - - - `data`: any data sctructure to be sent as JSON body. Put `nil` For - `GET` requests. - - - `result`: a symbol to bind the data from the HTTP response with - `let` form before executing the body. - - Example: - - (def driver (firefox)) - (println (execute {:driver driver - :method :get - :path [:session (:session driver) :element :active]))) - " + "Return response from having `:driver` execute HTTP `:method` request to `:path` with body `:data`. + + Response body automatically converted from JSON to a clojure keywordized map. + Any HTTP status failure code results in a throw. + + - `:method` HTTP method, e.g. `:get`, `:post`, `:delete`, etc. + - `:path` a vector of strings/keywords representing a server's path. For example: + - this: `[:session \"aaaa-bbbb-cccc\" :element \"dddd-eeee\" :find]` + - becomes: `\"/session/aaaa-bbbb-cccc/element/dddd-eeee/find\"`. + - `:data` optional body to send. Automatically converted to JSON. + + This can be useful to call when you want to invoke some WebDriver + implementation specific feature that Etaoin has not otherwise exposed. + + For example, if Etaoin did not already have [[get-url]]: + + ```Clojure + (def driver (e/firefox)) + (e/go driver \"https://clojure.org\") + (e/execute {:driver driver + :method :get + :path [:session (:session driver) :url]}) + ;; => {:value \"https://clojure.org/\"} + ```" [{:keys [driver method path data]}] (client/call driver method path data)) @@ -114,18 +230,22 @@ ;; (defn get-status - "Returns the current Webdriver status info. The content depends on - specific driver." + "Returns `driver` status. + + Indicates readiness to create a new session. + + The return varies for different driver implementations." [driver] (:value (execute {:driver driver :method :get :path [:status]}))) (defn create-session - "Initiates a new session for a driver. Opens a browser window as a - side-effect. All the further requests are made within specific - session. Some drivers may work with only one active session. Returns - a long string identifier." + "Have `driver` create a new session and return resulting session-id string. + + Opens a browser window as a side-effect (visible if not running headless). + Further requests to this driver will be for this session. + Etaoin assumes one active session per driver." [driver & [capabilities]] (let [data (if (= (dispatch-driver driver) :safari) {:capabilities (or capabilities {})} ;; required for safari even if empty @@ -138,7 +258,10 @@ (:sessionId (:value result))))) ;; firefox, safari (defn delete-session - "Deletes a session. Closes a browser window." + "Have `driver` delete the active session. + Closes the browser window. + + See also: [[quit]]." [driver] (execute {:driver driver :method :delete @@ -149,18 +272,14 @@ ;; (defmulti ^:private get-active-element* - "Returns the currect active element selected by mouse or a - keyboard (Tab, arrows)." - dispatch-driver) + "Have `driver` return the active element on the page. -(defmethod get-active-element* :firefox - [driver] - (-> (execute {:driver driver - :method :get - :path [:session (:session driver) :element :active]}) - :value first second)) + An active element is the one with the current focus. + It was selected for example by mouse click, a keyboard (tab, arrows), or `autofocus`." + dispatch-driver) -(defmethod get-active-element* :safari +(defmethods get-active-element* + [:firefox :safari] [driver] (-> (execute {:driver driver :method :get @@ -181,7 +300,7 @@ ;; (defmulti get-window-handle - "Returns the current active window handler as a string." + "Have `driver` return the current browser window handle string." {:arglists '([driver])} dispatch-driver) @@ -191,30 +310,20 @@ :method :get :path [:session (:session driver) :window_handle]}))) -(defmethod get-window-handle :safari - [driver] - (:value (execute {:driver driver - :method :get - :path [:session (:session driver) :window]}))) - -(defmethod get-window-handle :firefox +(defmethods get-window-handle + [:firefox :safari] [driver] (:value (execute {:driver driver :method :get :path [:session (:session driver) :window]}))) (defmulti get-window-handles - "Returns a vector of all window handlers." + "Have `driver` return a vector of all browser window handle strings." {:arglists '([driver])} dispatch-driver) -(defmethod get-window-handles :firefox - [driver] - (:value (execute {:driver driver - :method :get - :path [:session (:session driver) :window :handles]}))) - -(defmethod get-window-handles :safari +(defmethods get-window-handles + [:firefox :safari] [driver] (:value (execute {:driver driver :method :get @@ -228,7 +337,7 @@ :path [:session (:session driver) :window_handles]}))) (defmulti switch-window - "Switches a browser to another window." + "Have `driver` switch to browser window with `handle`." {:arglists '([driver handle])} dispatch-driver) @@ -249,6 +358,7 @@ :data {:name handle}})) (defn switch-window-next + "Have `driver` switch to next browser window." [driver] (let [current-handle (try (get-window-handle driver) @@ -262,7 +372,8 @@ (switch-window driver next-handle))) (defmulti close-window - "Closes the current browser window." + "Have `driver` close current browser window." + {:arglists '([driver])} dispatch-driver) (defmethod close-window :default @@ -272,17 +383,12 @@ :path [:session (:session driver) :window]})) (defmulti maximize - "Makes the browser window as wide as your screen allows." - {:arglists '([driver])} ;; todo it does't work + "Have `driver` make the current browser window as large as your screen allows." + {:arglists '([driver])} dispatch-driver) -(defmethod maximize :firefox - [driver] - (execute {:driver driver - :method :post - :path [:session (:session driver) :window :maximize]})) - -(defmethod maximize :safari +(defmethods maximize + [:firefox :safari] [driver] (execute {:driver driver :method :post @@ -297,19 +403,12 @@ :path [:session (:session driver) :window h :maximize]}))) (defmulti get-window-size - "Returns a window size a map with `:width` and `:height` keys." + "Have `driver` return the current browser window size in pixels as a map of `:width` and `:height`." {:arglists '([driver])} dispatch-driver) -(defmethod get-window-size :firefox - [driver] - (-> (execute {:driver driver - :method :get - :path [:session (:session driver) :window :rect]}) - :value - (select-keys [:width :height]))) - -(defmethod get-window-size :safari +(defmethods get-window-size + [:firefox :safari] [driver] (-> (execute {:driver driver :method :get @@ -327,20 +426,13 @@ (select-keys [:width :height])))) (defmulti get-window-position - "Returns a window position relative to your screen as a map with - `:x` and `:y` keys." + "Have `driver` return the current window position, in pixels relative to the screen, as a map of + `:x` and `:y`." {:arglists '([driver])} dispatch-driver) -(defmethod get-window-position :safari - [driver] - (-> (execute {:driver driver - :method :get - :path [:session (:session driver) :window :rect]}) - :value - (select-keys [:x :y]))) - -(defmethod get-window-position :firefox +(defmethods get-window-position + [:firefox :safari] [driver] (-> (execute {:driver driver :method :get @@ -382,7 +474,8 @@ :data {:width width :height height}}))) (defn set-window-size - "Sets new size for a window. Absolute precision is not guaranteed." + "Have `driver` set the `width` and `height` in pixels of the current window. + Absolute precision is not guaranteed." ([driver {:keys [width height]}] (set-window-size* driver width height)) ([driver width height] @@ -413,8 +506,10 @@ :data {:x x :y y}})))) (defn set-window-position - "Sets new position for a window. Absolute precision is not - guaranteed." + "Have `driver` set the `x` `y` position of the current browser window. + + Position is in pixels and relative to your screen. + Absolute precision is not guaranteed." ([driver {:keys [x y]}] (set-window-position* driver x y)) ([driver x y] @@ -426,13 +521,14 @@ ;; (defn go - "Open the `url` in the current window. + "Have `driver` open `url` in the current browser window. Example: + ```Clojure (def ff (firefox)) (go ff \"http://google.com\") - " + ```" [driver url] (execute {:driver driver :method :post @@ -440,23 +536,23 @@ :data {:url url}})) (defn back - "Move backwards in a browser's history." + "Have `driver` navigate backward in the browser history." [driver] (execute {:driver driver :method :post :path [:session (:session driver) :back]})) (defn refresh - "Reloads the current window." + "Have `driver` reload the content in the current browser window." [driver] (execute {:driver driver :method :post :path [:session (:session driver) :refresh]})) -(def reload refresh) ;; just an alias +(def ^{:arglists '([driver])} reload "Alias for [[refresh]]" refresh) (defn forward - "Move forwards in a browser's history." + "Have `driver` navigate forward in the browser's history." [driver] (execute {:driver driver :method :post @@ -467,14 +563,14 @@ ;; (defn get-url - "Returns the current URL string." + "Have `driver` return the current url location as a string." [driver] (:value (execute {:driver driver :method :get :path [:session (:session driver) :url]}))) (defn get-title - "Returns the current window's title." + "Have `driver` return the current page title." [driver] (:value (execute {:driver driver :method :get @@ -486,17 +582,8 @@ (defmulti ^:private find-element* dispatch-driver) -(defmethod find-element* :firefox - [driver locator term] - (-> (execute {:driver driver - :method :post - :path [:session (:session driver) :element] - :data {:using locator :value term}}) - :value - first - second)) - -(defmethod find-element* :safari +(defmethods find-element* + [:firefox :safari] [driver locator term] (-> (execute {:driver driver :method :post @@ -514,7 +601,7 @@ :data {:using locator :value term}}) :value :ELEMENT)) -(defmulti find-elements* dispatch-driver) +(defmulti ^:no-doc find-elements* dispatch-driver) (defmethod find-elements* :default [driver locator term] @@ -527,18 +614,8 @@ (defmulti ^:private find-element-from* dispatch-driver) -(defmethod find-element-from* :firefox - [driver el locator term] - {:pre [(some? el)]} - (-> (execute {:driver driver - :method :post - :path [:session (:session driver) :element el :element] - :data {:using locator :value term}}) - :value - first - second)) - -(defmethod find-element-from* :safari +(defmethods find-element-from* + [:firefox :safari] [driver el locator term] {:pre [(some? el)]} (-> (execute {:driver driver @@ -576,21 +653,30 @@ ;; (defn query - "Finds an element on a page. - - A query might be: - - - a string with an XPath expression; - - a keyword `:active` that means to get the current active element; - - any other keyword which stands for an element's ID attribute; - - a map with either `:xpath` or `:css` key with a string value - of corresponding selector type (XPath or CSS); - - any other map that will be expanded into XPath term (see README.md); - - a vector of any expressions mentioned above. In that case, each next - term is searched from the previous one. - - Returns a element's unique identifier." - + "Use `driver` to find and return the first element on current page matching `q`. + + Query `q` can be: + + - `:active` the current active element + - a keyword to find element by it's ID attribute: + - `:my-id` + - (use `{:id \"my-id\"}` for ids that cannot be represented as keywords) + - an XPath expression: + - `\".//input[@id='uname']\"` + - a map with either `:xpath` or `:css`: + - `{:xpath \".//input[@id='uname']\"`}` + - `{:css \"input#uname[name='username']\"}` + - any other map is converted to an XPath expression: + - `{:tag :div}` + - is equivalent to XPath: `\".//div\"` + - multiple of the above (wrapped in a vector or not). + The result of each expression is fed into the next. + - `{:tag :div} \".//input[@id='uname']\"` + - `[{:tag :div} \".//input[@id='uname']\"]` + + Returns the found element's unique identifier, or throws when not found. + + See [Selecting Elements](/doc/01-user-guide.adoc#querying) for more details." ([driver q] (cond @@ -611,10 +697,9 @@ (reduce folder (query driver q) more)))) (defn query-all - "Finds multiple elements on a page. - See `query` function for incoming params. - Returns a vector of element identifiers." + "Use `driver` to return a vector of all elements on current page matching `q`. + See [[query]] for details on `q`." ([driver q] (cond @@ -631,17 +716,13 @@ (find-elements-from* driver el loc term)))) (defn query-tree - "Takes selectors and acts like a tree. - Every next selector queries elements from the previous ones. - - {:tag :div} {:tag :a} - means - {:tag :div} -> [div1 div2 div3] - div1 -> [a1 a2 a3] - div2 -> [a4 a5 a6] - div3 -> [a7 a8 a9] - so the result will be #{a1 ... a9} - " + "Use `driver` to return a collection of all elements matching piped queries. + + The results of `q` are queried by `qs1` which in turn are queried by `qs2`, and so on. + + See [[query]] for details on `q`. + + See [User Guide](/doc/01-user-guide.adoc#query-tree) for an example." [driver q & qs] (reduce (fn [elements q] (let [[loc term] (query/expand driver q)] @@ -653,14 +734,18 @@ qs)) (defn child - "Finds a single element under given root element." + "Uses `driver` to return single element satisfying query `q` under given `ancestor-el` element. + + See [[query]] for details on `q`." [driver ancestor-el q] {:pre [(some? ancestor-el)]} (let [[loc term] (query/expand driver q)] (find-element-from* driver ancestor-el loc term))) (defn children - "Finds multiple elements under given root element." + "Use `driver` to return a vector of unique elements satisfying query `q` under given `ancestor-el` element. + + See [[query]] for details on `q`." [driver ancestor-el q] {:pre [(some? ancestor-el)]} (let [[loc term] (query/expand driver q)] @@ -671,139 +756,173 @@ (declare el->ref wait) (defn rand-uuid + "Return a random UUID string." [] (str (java.util.UUID/randomUUID))) (defn make-action-input + "Return a new action input source of `type`." [type] {:type (name type) :id (rand-uuid) :actions []}) (defn make-pointer-input + "Return a new pointer input source of pointer `type`." [type] (-> (make-action-input :pointer) (assoc-in [:parameters :pointerType] type))) (defn make-mouse-input + "Return a new mouse input source." [] (make-pointer-input :mouse)) (defn make-touch-input + "Return a new touch input source." [] (make-pointer-input :touch)) (defn make-pen-input + "Return a new pen input source." [] (make-pointer-input :pen)) (defn make-key-input + "Return a new key input source." [] (make-action-input :key)) (defn add-action + "Return `input` with `action` added." [input action] (update input :actions conj action)) (defn add-pause + "Return `input` source with a pause action added. + + Optionally specify a `duration`, defaults to 0." [input & [duration]] (add-action input {:type "pause" :duration (or duration 0)})) (defn add-double-pause + "Return `input` source with two pause actions added. + + Optionally specify a `duration`, defaults to 0." [input & [duration]] (-> input (add-pause duration) (add-pause duration))) (defn add-key-down + "Return `input` source with `key` down action added." [input key] (add-action input {:type "keyDown" :value key})) (defn add-key-up + "Return `input` source with `key` up action added." [input key] (add-action input {:type "keyUp" :value key})) (defn add-key-press + "Return `input` source with `key` down and up actions added." [input key] (-> input (add-key-down key) (add-key-up key))) (defn add-pointer-down - [input & [button]] + "Return `input` source with `pointer-button` down action added. + + `button` defaults to [[etaoin.keys/mouse-left]]" + [input & [pointer-button]] (add-action input {:type "pointerDown" :duration 0 - :button (or button k/mouse-left)})) + :button (or pointer-button k/mouse-left)})) (defn add-pointer-up - [input & [button]] + "Return `input` source with `pointer-button` up action added. + + `button` defaults to [[etaoin.keys/mouse-left]]" + [input & [pointer-button]] (add-action input {:type "pointerUp" :duration 0 - :button (or button k/mouse-left)})) + :button (or pointer-button k/mouse-left)})) (defn add-pointer-cancel + "Return `input` with a pointer cancel action added." [input] (add-action input {:type "pointerCancel"})) -(def default-duration 250) -(def default-origin "viewport") - (defn add-pointer-move - " - Move the pointer from `origin` to `x` and `y` offsets - with `duration` in milliseconds. - - Possible `origin` values are: - - - 'viewport'; the final x axis will be equal to `x` offset - and the final y equal to `y` offset. This is the default - value. - - - 'pointer'; the final x will be equal to start x of pointer + `x` offset - and the final y equal to start y of pointer + `y` offset. - - - a map that represents a web element. To get it, pass the result - of the `query` function into the `el->ref`, for example: + "Return `input` source with pointer move action added. - (el->ref (query driver q)) + Moves the pointer from `origin` to offset `x` `y` with optional `duration` in milliseconds. - where `q` is a query term to find an element. - " + Possible `origin` values are: + - `\"viewport\"` move to `x` `y` offset in viewport. This is the default. + - `\"pointer\"` `x` `y` are interpreted as offsets from the current pointer location. + - a map that represents a web element for example via [[el->ref]]: + ```Clojure + (el->ref (query driver q)) + ``` + where `q` is [[query]] to find the element. + + Optionally specify `:duration` in milliseconds, defaults to 250." [input & [{:keys [x y origin duration]}]] - (add-action input {:type "pointerMove" - :x (or x 0) - :y (or y 0) - :origin (or origin default-origin) - :duration (or duration default-duration)})) + (let [default-duration 250 + default-origin "viewport"] + (add-action input {:type "pointerMove" + :x (or x 0) + :y (or y 0) + :origin (or origin default-origin) + :duration (or duration default-duration)}))) (defn add-pointer-move-to-el + "Return `input` source with pointer move to element `el` action added. + + Optionally specify `:duration` in milliseconds, defaults to 250." [input el & [{:keys [duration]}]] (add-pointer-move input {:duration duration :origin (el->ref el)})) (defn add-pointer-click - [input & [button]] + "Return `input` source with `pointer-button` down and up actions added. + + `pointer-button` defaults to [[etaoin.keys/mouse-left]]" + [input & [pointer-button]] (-> input - (add-pointer-down button) - (add-pointer-up button))) + (add-pointer-down pointer-button) + (add-pointer-up pointer-button))) (defn add-pointer-click-el - [input el & [button]] + "Return `input` source with `pointer-button` down and up actions on element `el` added. + + `pointer-button` defaults to [[etaoin.keys/mouse-left]]" + [input el & [pointer-button]] (-> input (add-pointer-move-to-el el) - (add-pointer-click button))) + (add-pointer-click pointer-button))) (defn add-pointer-double-click - [input & [button]] + "Return `input` source with `pointer-button` down, up, down, up actions added. + + `pointer-button` defaults to [[etaoin.keys/mouse-left]]" + [input & [pointer-button]] (-> input - (add-pointer-click button) - (add-pointer-click button))) + (add-pointer-click pointer-button) + (add-pointer-click pointer-button))) (defn add-pointer-double-click-el - [input el & [button]] + "Return `input` source with `pointer-button` down, up, down, up actions on element `el` added. + + `pointer-button` defaults to [[etaoin.keys/mouse-left]]" + [input el & [pointer-button]] (-> input (add-pointer-move-to-el el) - (add-pointer-double-click button))) + (add-pointer-double-click pointer-button))) (defmacro with-key-down + "Returns `input` source piped through `key` down, + then presumably a `body` of more actions then a `key` up action." [input key & body] `(-> ~input (add-key-down ~key) @@ -811,20 +930,31 @@ (add-key-up ~key))) (defmacro with-pointer-btn-down - [input button & body] + "Returns `input` source piped through `pointer-button` down action, + then presumably a `body` of more actions then a pointer `pointer-button` up action. + + `pointer-button` should be, for example, [[etaoin.keys/mouse-left]]" + [input pointer-button & body] `(-> ~input - (add-pointer-down ~button) + (add-pointer-down ~pointer-button) ~@body - (add-pointer-up ~button))) + (add-pointer-up ~pointer-button))) (defmacro with-pointer-left-btn-down + "Returns `input` source piped through pointer left button down action, + then presumably a `body` of more actions then a pointer left button up action." [input & body] `(-> ~input add-pointer-down ~@body add-pointer-up)) -(defmulti perform-actions dispatch-driver) +(defmulti perform-actions + "Have `driver` perform actions defined in `input` source(s) simultaneously. + + See [Actions](/doc/01-user-guide.adoc#actions) for more details." + {:arglists '([driver input & inputs])} + dispatch-driver) (defmethod perform-actions :default @@ -839,7 +969,11 @@ [_driver _input & _inputs] (util/error "Phantom doesn't support w3c actions.")) -(defmulti release-actions dispatch-driver) +(defmulti release-actions + "Have `driver` clear any active action state. + This includes any key presses and/or a pointer button being held down." + {:arglists '([driver])} + dispatch-driver) (defmethod release-actions :default @@ -848,7 +982,6 @@ :method :delete :path [:session (:session driver) :actions]})) - (defmethod release-actions :phantom [_driver _input & _inputs] @@ -859,7 +992,7 @@ ;; (defmulti mouse-btn-down - "Puts down a button of a virtual mouse." + "Have `driver` press down on the button of the virtual mouse." {:arglists '([driver])} dispatch-driver) @@ -871,7 +1004,7 @@ :path [:session (:session driver) :buttondown]})) (defmulti mouse-btn-up - "Puts up a button of a virtual mouse." + "Have `driver` release button of the virtual mouse." {:arglists '([driver])} dispatch-driver) @@ -883,8 +1016,8 @@ :path [:session (:session driver) :buttonup]})) (defmulti mouse-move-to - "Moves a virtual mouse pointer either to an element - or by `x` and `y` offset." + "Have `driver` moves the virtual mouse pointer either to an element found by query `q` + or to a specific by `x` and `y` pixel offset within the browser window." {:arglists '([driver q] [driver x y])} dispatch-driver) @@ -902,7 +1035,7 @@ :data {:xoffset x :yoffset y}}))) (defmacro with-mouse-btn - "Performs the body keeping mouse botton pressed." + "Have `driver` press the virtual mouse button, execute `body`, then release the virtual mouse button." [driver & body] `(do (mouse-btn-down ~driver) @@ -912,28 +1045,18 @@ (mouse-btn-up ~driver))))) (defmulti drag-and-drop - "Performs drag and drop operation as a sequence of the following steps: + "Have `driver` perform a drag and drop from element found by `q-from` to element found by `q-to`: 1. moves mouse pointer to an element found with `q-from` query; - 2. puts down mouse button; + 2. holds down the mouse button; 3. moves mouse to an element found with `q-to` query; - 4. puts up mouse button. - - Arguments: + 4. releases the mouse button. - - `driver`: a driver instance, - - - `q-from`: from what element to start dragging; any expression that - `query` function may accept; - - - `q-to`: to what element to drag, a seach term. + See [[query]] for details on `q-from`, `q-to`. Notes: - - does not work in Phantom.js since it does not have a virtual mouse API; - - - does not work in Safari. - " + - does not work in Safari." {:arglists '([driver q-from q-to])} dispatch-driver) @@ -959,9 +1082,8 @@ ;; Clicking ;; - (defn click-el - "Click on an element having its system id." + "Have `driver` click on element `el`." [driver el] {:pre [(some? el)]} (execute {:driver driver @@ -969,16 +1091,18 @@ :path [:session (:session driver) :element el :click]})) (defn click - "Clicks on an element (a link, a button, etc)." + "Have `driver` click on element found by query `q`. + + See [[query]] for details on `q`." [driver q] (click-el driver (query driver q))) (defn click-single - " - Click on an element checking that there is only one element found. - Throw an exception otherwise. - " + "Have `driver` click on element found by query `q`. + If `q` returns more than one element, throws. + + See [[query]] for details on `q`." [driver q] (let [elements (query-all driver q)] (if (> (count elements) 1) @@ -988,11 +1112,12 @@ (click-el driver (first elements))))) (defn click-multi - "Clicks on a multiple elements in batch. + "Have `driver` click on first element found by each query in vector `qs`. - `qs` a vector of [query1 query2 query3 ...] - `pause` a pause between click in seconds, default is 0" + - `qs` a vector of queries `[query1 query2 query3 ...]` + - `pause` a pause prior to each click in seconds, default is `0`. + See [[query]] for details on `q`s." [driver qs & [pause]] (doseq [q qs] (wait (or pause 0)) @@ -1000,7 +1125,14 @@ ;; Double click -(defmulti double-click-el dispatch-driver) +(defmulti double-click-el + "Have `driver` double-click on element `el`. + + Note: + - the supported browsers are Chrome, and Phantom.js. + - for Firefox and Safari, your may try to simulate it as a `click, wait, click` sequence." + {:arglists '([driver el])} + dispatch-driver) (defmethod double-click-el :default @@ -1014,13 +1146,13 @@ (perform-actions driver mouse))) (defn double-click - "Performs double click on an element. + "Have `driver` double-click on element found by query `q`. - Note: + See [[query]] for details on `q`. - the supported browsers are Chrome, and Phantom.js. - For Firefox and Safari, your may try to simulate it as a `click, wait, click` - sequence." + Note: + - the supported browsers are Chrome, and Phantom.js. + - for Firefox and Safari, your may try to simulate via `click, wait, click` sequence." [driver q] (double-click-el driver (query driver q))) @@ -1028,85 +1160,84 @@ ;; Blind click (defmulti mouse-click - " - Click on a mouse button using the *current* mouse position. - The `btn` is a mouse button code. See `keys/mouse-*` constants. - " - {:arglists '([driver btn])} + "Have `driver` click `mouse-button` at current mouse pointer position. + + `mouse-button` should be `etaoin.keys/mouse-left`,`etaoin.keys/mouse-middle` or `etaoin.keys/mouse-right`." + {:arglists '([driver mouse-button])} dispatch-driver) (defmethod mouse-click :default - [driver btn] + [driver mouse-button] (let [{driver-type :type} driver] (throw (ex-info "Mouse click is not supported for that browser" - {:button btn + {:button mouse-button :driver-type driver-type})))) (defmethod mouse-click :chrome ;; TODO: try safari once the issue with it is solved - [driver btn] + [driver mouse-button] (execute {:driver driver :method :post :path [:session (:session driver) :click] - :data {:button btn}})) + :data {:button mouse-button}})) (defn left-click - "A shortcut for `mouse-click` with the left button." + "Have `driver` click the left mouse button at current mouse pointer position." [driver] (mouse-click driver k/mouse-left)) (defn right-click - "A shortcut for `mouse-click` with the right button." + "Have `driver` click the right mouse button at current mouse pointer position." [driver] (mouse-click driver k/mouse-right)) (defn middle-click - "A shortcut for `mouse-click` with the middle button." + "Have `driver` click the middle mouse button at current mouse pointer position." [driver] (mouse-click driver k/mouse-middle)) (defn mouse-click-on - " - Mouse click on a specific element and a button. - Moves the mouse pointer to the element first. - " + "Have `driver` move mouse pointer to element found by `q` then click `mouse-button`. + + - `mouse-button` should be `etaoin.keys/mouse-left`,`etaoin.keys/mouse-middle` or `etaoin.keys/mouse-right`. + - `q` see [[query]] for details." [driver btn q] (doto driver (mouse-move-to q) (mouse-click btn))) (defn left-click-on - " - Left mouse click on an element. Probably don't need - that one, use `click` instead. - " + "Have `driver` move mouse pointer to element found by `q` then click left mouse button. + + See [[query]] for details on `q`." [driver q] (mouse-click-on driver k/mouse-left q)) (defn right-click-on - " - Move pointer to an element found with a query - and right click on it. - " + "Have `driver` move mouse pointer to element found by `q` then click right mouse button. + + See [[query]] for details on `q`." [driver q] (mouse-click-on driver k/mouse-right q)) (defn middle-click-on - " - Move pointer to an element found with a query - and middle click on it. Useful for opening links - in a new tab. - " + "Have `driver` move mouse pointer to element found by `q` then click middle mouse button. + + See [[query]] for details on `q`. + + Useful for opening links in a new tab." [driver q] (mouse-click-on driver k/mouse-middle q)) - ;; ;; Element size ;; -(defmulti get-element-size-el dispatch-driver) +(defmulti get-element-size-el + "Have `driver` return map of `:width` and `:height`, in pixels, of element `el`." + {:arglists '([driver el])} + dispatch-driver) (defmethods get-element-size-el [:chrome :edge :phantom] @@ -1118,16 +1249,8 @@ :value (select-keys [:width :height]))) -(defmethod get-element-size-el :firefox - [driver el] - {:pre [(some? el)]} - (-> (execute {:driver driver - :method :get - :path [:session (:session driver) :element el :rect]}) - :value - (select-keys [:width :height]))) - -(defmethod get-element-size-el :safari +(defmethods get-element-size-el + [:firefox :safari] [driver el] {:pre [(some? el)]} (-> (execute {:driver driver @@ -1137,7 +1260,9 @@ (select-keys [:width :height]))) (defn get-element-size - "Returns an element size as a map with :width and :height keys." + "Have `driver` return map of `:width` and `:height`, in pixels, of element found by query `q`. + + See [[query]] for details on `q`." [driver q] (get-element-size-el driver (query driver q))) @@ -1145,7 +1270,10 @@ ;; element location ;; -(defmulti get-element-location-el dispatch-driver) +(defmulti get-element-location-el + "Have `driver` return map of `:x` `:y` offset, in pixels of element `el`." + {:arglists '([driver el])} + dispatch-driver) (defmethods get-element-location-el [:chrome :edge :phantom] @@ -1157,16 +1285,8 @@ :value (select-keys [:x :y]))) -(defmethod get-element-location-el :firefox - [driver el] - {:pre [(some? el)]} - (-> (execute {:driver driver - :method :get - :path [:session (:session driver) :element el :rect]}) - :value - (select-keys [:x :y]))) - -(defmethod get-element-location-el :safari +(defmethods get-element-location-el + [:firefox :safari] [driver el] {:pre [(some? el)]} (-> (execute {:driver driver @@ -1176,7 +1296,9 @@ (select-keys [:x :y]))) (defn get-element-location - "Returns an element location on a page as a map with :x and :x keys." + "Have `driver` return map of `:x` `:y` offset, in pixels of element found by query `q`. + + See [[query]] for details on `q`." [driver q] (get-element-location-el driver (query driver q))) @@ -1185,7 +1307,9 @@ ;; (defn get-element-box - "Returns a bounding box for an element found with a query term. + "Have `driver` return map describing a bounding box for element found by query `q`. + + See [[query]] for details on `q`. The result is a map with the following keys: @@ -1194,8 +1318,7 @@ - `:x2`: bottom right `x` coordinate; - `:y2`: bottom right `y` coordinate; - `:width`: width as a difference b/w `:x2` and `:x1`; - - `:height`: height as a difference b/w `:y2` and `:y1`. - " + - `:height`: height as a difference b/w `:y2` and `:y1`. " [driver q] (let [el (query driver q) {:keys [width height]} (get-element-size-el driver el) @@ -1208,18 +1331,10 @@ :height height})) (defn intersects? - "Determines whether two elements intersects in geometry meaning. + "Return true if bounding boxes found by `driver` for element found by query `q1` + intersects element found by query `q2`. - The implementation compares bounding boxes for each element - analyzing their arrangement. - - Arguments: - - - `q1` and `q2` are query terms to find elements to check for - intersection. - - Returns true or false. - " + Compares bounding boxes of found elements." [driver q1 q2] (let [a (get-element-box driver q1) b (get-element-box driver q2)] @@ -1232,7 +1347,8 @@ ;; properties ;; -(defn- get-element-property-el +(defn get-element-property-el + "Have `driver` return value for `property` of element `el`." [driver el property] {:pre [(some? el)]} (:value (execute {:driver driver @@ -1240,28 +1356,26 @@ :path [:session (:session driver) :element el :property (name property)]}))) (defn get-element-property - "Returns a property of an element (value, etc). + "Have `driver` value for `property` of element found by query `q`. - Arguments: - - - `driver`: a driver instance, + Found property value is returned as string. + When element is found but property is absent, returns `nil`. - - `q`: a query term to find an element, + See [[query]] for details on `q`." + [driver q property] + (get-element-property-el driver (query driver q) property)) - - `name`: either a string or a keyword with a name of a property. +(defn get-element-properties + "Have `driver` return a vector of property values matching `properties` of element found by query `q`. - Returns: a string with the attribute value, `nil` if no such - property for that element." - [driver q name] - (get-element-property-el driver (query driver q) name)) + Found property values are returned as strings. + When element is found but property is absent, result is included in vector as `nil`. -(defn get-element-properties - "Returns multiple properties in batch. The result is a vector of - corresponding properties." - [driver q & props] + See [[query]] for details on `q`." + [driver q & properties] (let [el (query driver q)] (vec - (for [prop props] + (for [prop properties] (get-element-property-el driver el prop))))) ;; @@ -1269,101 +1383,119 @@ ;; (defn get-element-attr-el - [driver el attr] + "Have `driver` return value for `attribute` of element `el`, or `nil` if attribute does not exist." + [driver el attribute] {:pre [(some? el)]} (:value (execute {:driver driver :method :get - :path [:session (:session driver) :element el :attribute (name attr)]}))) + :path [:session (:session driver) :element el :attribute (name attribute)]}))) (defn get-element-attr - "Returns an HTTP attribute of an element (class, id, href, etc). - - Arguments: - - - `driver`: a driver instance, - - - `q`: a query term to find an element, + "Have `driver` return value for `attribute` of element found by `q`. - - `name`: either a string or a keyword with a name of an attribute. + Found attribute value is returned as string. + When element is found but attribute is absent, returns `nil`. - Returns: a string with the attribute value, `nil` if no such - attribute for that element. + See [[query]] for details on `q`. - Note: it does not split CSS classes! A single string with spaces is - returned. + Note: there is no special treatment of the `class` attribute. + A single string with spaces is returned. Example: + ```Clojure (def driver (firefox)) (get-element-attr driver {:tag :a} :class) - >> \"link link__external link__button\" ;; see note above - " - [driver q name] - (get-element-attr-el driver (query driver q) name)) + ;; => \"link link__external link__button\" + ```" + [driver q attribute] + (get-element-attr-el driver (query driver q) attribute)) (defn get-element-attrs - "Returns multiple attributes in batch. The result is a vector of - corresponding attributes." - [driver q & attrs] + "Have `driver` return values for `attributes` of element found by query `q`. + + Found attribute values are returned as strings. + When element is found but attribute is absent, result is included in vector as `nil`. + + See [[query]] for details on `q`." + [driver q & attributes] (let [el (query driver q)] (vec - (for [attr attrs] + (for [attr attributes] (get-element-attr-el driver el attr))))) ;; ;; css ;; -(defn- get-element-css-el - [driver el name*] +(defn get-element-css-el + "Have `driver` return value for CSS style `property` of element `el`, or `nil` if property does not exist." + [driver el property] {:pre [(some? el)]} (-> (execute {:driver driver :method :get - :path [:session (:session driver) :element el :css (name name*)]}) + :path [:session (:session driver) :element el :css (name property)]}) :value not-empty)) (defn get-element-css - "Returns a CSS property of an element. The property might be both - own or inherited. + "Have `driver` return the CSS style `property` of element found by query `q`. - Arguments: - - - `driver`: a driver instance, + `property` is a string/keyword representing a CSS name. + Examples: `:font` or `\"background-color\"` - - `q`: a query term, + The property will be returned if it was defined for the element itself or inherited. - - `name`: a string/keyword with a CSS name (:font, \"background-color\", etc). + Found property value is returned as string. + When element is found but property is absent, returns `nil`. - Returns a string with a value, `nil` if there is no such property. + See [[query]] for details on `q`. - Note: colors, fonts and some other properties may be represented in - their own ways depending on a browser. + Note: colors, fonts and some other properties may be represented differently for different browsers. Example: - + ```Clojure (def driver (firefox)) + (e/go driver \"https://clojars.org\") (get-element-css driver {:id :content} :background-color) - >> \"rgb(204, 204, 204)\" ;; or \"rgba(204, 204, 204, 1)\" - " - [driver q prop] - (get-element-css-el driver (query driver q) prop)) + ;; => \"rgb(226, 228, 227)\" + ```" + [driver q property] + (get-element-css-el driver (query driver q) property)) (defn get-element-csss - "Returns multiple CSS properties in batch. The result is a vector of - corresponding properties." - [driver q & props] + "Have `driver` return CSS style properties matching `properties` of element found by query `q`. + + See [[query]] for details on `q`. + + Found property values are returned as strings. + When element is found but property is absent, result is included in vector as `nil`. + + Note: colors, fonts and some other properties may be represented differently for different browsers. + + Example: + ```Clojure + (def driver (firefox)) + (e/go driver \"https://clojars.org\") + (e/get-element-csss driver {:tag :body} :background-color :typo :line-height :font-size) + ;; => [\"rgb(226, 228, 227)\" nil \"20px\" \"14px\"] + ```" + [driver q & properties] (let [el (query driver q)] (vec - (for [prop props] + (for [prop properties] (get-element-css-el driver el prop))))) ;; ;; element inner HTML ;; (defmulti get-element-inner-html-el - "Returns element's inner text by its identifier." + "Have `driver` return inner text of element `el`. + + For element with `my-id` in `

hello

` return will be + `\"

hello

\"`." + {:arglists '([driver el])} dispatch-driver) (defmethod get-element-inner-html-el @@ -1383,11 +1515,12 @@ :path [:session (:session driver) :element el :attribute :innerHTML]}))) (defn get-element-inner-html - "Returns element's inner HTML. + "Have `driver` return inner text of element found by query `q`. - For element `el` in `

hello

` it will - be \"

hello

\" string. - " + See [[query]] for details on `q`. + + For element with `my-id` in `

hello

` return will be + `\"

hello

\"`." [driver q] (get-element-inner-html-el driver (query driver q))) @@ -1396,7 +1529,7 @@ ;; (defn get-element-tag-el - "Returns element's tag name by its identifier." + "Have `driver` return tag name of element `el`." [driver el] {:pre [(some? el)]} (:value (execute {:driver driver @@ -1404,12 +1537,16 @@ :path [:session (:session driver) :element el :name]}))) (defn get-element-tag - "Returns element's tag name (\"div\", \"input\", etc)." + "Have `driver` return tag name of element found by query `q`. + + See [[query]] for details on `q`." [driver q] (get-element-tag-el driver (query driver q))) (defn get-element-text-el - "Returns element's inner text by its identifier." + "Have `driver` return text of element `el`. + + Text return for `

hello

` is `\"hello\"`." [driver el] {:pre [(some? el)]} (:value (execute {:driver driver @@ -1417,10 +1554,11 @@ :path [:session (:session driver) :element el :text]}))) (defn get-element-text - "Returns inner element's text. + "Have `driver` return inner text of element found by query `q`. - For `

hello

` it will be \"hello\" string. - " + See [[query]] for details on `q`. + + Text return for `

hello

` is `\"hello\"`." [driver q] (get-element-text-el driver (query driver q))) @@ -1428,8 +1566,10 @@ ;; Element value ;; -(defn- get-element-value-el - "Low level: returns element's value by its identifier." +(defn get-element-value-el + "Have `driver` return the value of element `el`. + + To be used on input elements." [driver el] {:pre [(some? el)]} (:value (execute {:driver driver @@ -1437,7 +1577,11 @@ :path [:session (:session driver) :element el :value]}))) (defmulti get-element-value - "Returns the current element's value (input text)." + "Have `driver` return the value of element found by query `q`. + + To be used on input elements. + + See [[query]] for details on `q`." {:arglists '([driver q])} dispatch-driver) @@ -1451,47 +1595,38 @@ [driver q] (get-element-attr driver q :value)) -(defmethod get-element-value - :firefox - [driver q] - (get-element-property driver q :value)) - -(defmethod get-element-value - :safari +(defmethods get-element-value + [:firefox :safari] [driver q] (get-element-property driver q :value)) ;; -;; cookes +;; cookies ;; (defn get-cookies - "Returns all the cookies browser keeps at the moment. + "Have `driver` return a vector of current browser cookies. Each cookie is a map with structure: + ```Clojure {:name \"cookie1\", - :value \"test1\", - :path \"/\", - :domain \"\", - :expiry nil, - :secure false, - :httpOnly false} - " + :value \"test1\", + :path \"/\", + :domain \"\", + :expiry nil, + :secure false, + :httpOnly false} + ```" [driver] (:value (execute {:driver driver :method :get :path [:session (:session driver) :cookie]}))) (defn get-cookie - "Returns the first cookie with such name. - - Arguments: - - - `driver`: a driver instance, + "Have `driver` return first cookie matching `cookie-name`. - - `cookie-name`: a string/keyword witn a cookie name. - " + When `cookie-name` is a keyword it will be converted appropriately." [driver cookie-name] (->> driver get-cookies @@ -1499,15 +1634,10 @@ first)) (defn set-cookie - "Sets a new cookie. - - Arguments: + "Have `driver` set a `cookie`. - - `driver`: a driver instance, - - - `cookie`: a map with structure described in `get-cookies`. At - least `:name` and `:value` fields should be populated. - " + `cookie` is a map with structure described in [[get-cookies]]. + At least `:name` and `:value` keys should be populated." [driver cookie] (execute {:driver driver :method :post @@ -1515,14 +1645,14 @@ :data {:cookie cookie}})) (defn delete-cookie - "Deletes a cookie by its name." + "Have `driver` delete cookie with cookie `cookie-name`." [driver cookie-name] (execute {:driver driver :method :delete :path [:session (:session driver) :cookie (name cookie-name)]})) (defmulti delete-cookies - "Deletes all the cookies for all domains." + "Have `driver` delete all browser cookies for all domains." {:arglists '([driver])} dispatch-driver) @@ -1545,7 +1675,7 @@ ;; (defn get-source - "Returns browser's current HTML markup as a string." + "Have `driver` return browser's current page HTML markup as a string." [driver] (:value (execute {:driver driver :method :get @@ -1556,34 +1686,33 @@ ;; (defn el->ref - "Turns machinery-wise element ID into an object - that Javascript use to reference existing DOM element. + "Return map representing an element reference for WebDriver. - The magic constant below is taken from the standard: - https://www.w3.org/TR/webdriver/#elements + The magic `:element-` constant in source is taken from the [WebDriver Spec](https://www.w3.org/TR/webdriver/#elements). - Passing such an object to `js-execute` automatically expands into a - DOM node. For example: + Passing the element reference map to `js-execute` automatically expands it + into a DOM node. For example: - ;; returns long UUID + ```Clojure + ;; returns UUID string for the element (def el (query driver :button-ok)) ;; the first argument will the an Element instance. (js-execute driver \"arguments[0].scrollIntoView()\", (el->ref el)) - " + ```" [el] {:ELEMENT el :element-6066-11e4-a52e-4f735466cecf el}) (defmulti js-execute - "Executes Javascript code in browser synchronously. + "Return result of `driver` executing Javascript `script` with `args` synchronously in the browser. - The code is sent as a string (might be multi-line). Under the hood, - a browser wraps your code into a function so avoid using `function` + The script is sent as a string (can be multi-line). Under the hood, + the browser wraps your code into a function, so avoid using the `function` clause at the top level. Don't forget to add `return ` operator if you are - interested in the result value. + interested in a resulting value. You may access arguments through the built-in `arguments` pseudo-array from your code. You may pass any data structures that @@ -1593,21 +1722,14 @@ pipeline (JS objects turn to Clojure maps, arrays into vectors and so on). - Arguments: - - - `driver`: a driver instance, - - - `script`: a string with the code to execute. - - - `args`: additional arguments for your code. Any data that might be - serialized into JSON. + - `args`: additional arguments for your script. Automatically converted to JSON. Example: - + ```Clojure (def driver (chrome)) (js-execute driver \"return arguments[0] + 1;\" 42) - >> 43 - " + ;; => 43 + ```" {:arglists '([driver script & args])} dispatch-driver) @@ -1618,14 +1740,8 @@ :path [:session (:session driver) :execute] :data {:script script :args (vec args)}}))) -(defmethod js-execute :firefox - [driver script & args] - (:value (execute {:driver driver - :method :post - :path [:session (:session driver) :execute :sync] - :data {:script script :args (vec args)}}))) - -(defmethod js-execute :safari +(defmethods js-execute + [:firefox :safari] [driver script & args] (:value (execute {:driver driver :method :post @@ -1633,21 +1749,27 @@ :data {:script script :args (vec args)}}))) (defmulti js-async - "Executes an asynchronous script in the browser and returns the result. - An asynchronous script is a such one that performs any kind of IO operations, - say, AJAX request to the server. When running such kind of a script, you cannot - just use the `return` statement like you do in ordinary scripts. Instead, the - driver passes a special handler as the last argument that should be called + "Return result of `driver` executing JavaScript `script` with `args` asynchornously in the browser. + + Executes an asynchronous script in the browser and returns the result. + An asynchronous script one that typically performs some kind of IO operation, + like an AJAX request to the server. You cannot just use the `return` statement + like you do in synchronous scripts. + + The driver passes a special handler as the last argument that should be called to return the final result. *Note:* calling this function requires the `script` timeout to be set properly, - meaning non-zero positive value. See `get-script-timeout`, `get-script-timeout` - and `with-script-timeout` functions/macroses. + meaning non-zero positive value. See [[get-script-timeout]], [[set-script-timeout]] + and [[with-script-timeout]]. + + - `args`: additional arguments for your code. Automatically converted to JSON. Example of a script: + ```Clojure // the `arguments` would be an array of something like: - // [1, 2, true, ..., ] + // [1, 2, true, ..., ] var callback = arguments[arguments.length-1]; @@ -1660,16 +1782,7 @@ callback({error: getErrorData(result)}); } }}); - - Arguments: - - - `driver`: a driver instance, - - - `script`: a string with the code to execute. - - - `args`: additional arguments for your code. Any data that might be - serialized into JSON. - " + ```" {:arglists '([driver script & args])} dispatch-driver) @@ -1680,26 +1793,21 @@ :path [:session (:session driver) :execute_async] :data {:script script :args (vec args)}}))) -(defmethod js-async :firefox - [driver script & args] - (:value (execute {:driver driver - :method :post - :path [:session (:session driver) :execute :async] - :data {:script script :args (vec args)}}))) - -(defmethod js-async :safari +(defmethod js-async + [:firefox :safari] [driver script & args] (:value (execute {:driver driver :method :post :path [:session (:session driver) :execute :async] :data {:script script :args (vec args)}}))) - ;; ;; User-Agent ;; -(defn get-user-agent [driver] +(defn get-user-agent + "Have `driver` return the browser `User-Agent`" + [driver] (js-execute driver "return navigator.userAgent")) @@ -1708,10 +1816,13 @@ ;; (defn js-localstorage-clear + "Have `driver` clear local storage." [driver] (js-execute driver "localStorage.clear()")) -(defn add-script [driver url] +(defn add-script + "Have `driver` add script with src `url` to page." + [driver url] (let [script (str "var s = document.createElement('script');" "s.type = 'text/javascript';" @@ -1724,28 +1835,27 @@ ;; (defn scroll - "Scrolls the window into absolute position (jumps to exact place)." + "Have `driver` scroll page to `x` `y` absolute pixel coordinates." ([driver x y] (js-execute driver "window.scroll(arguments[0], arguments[1]);" x y)) ([driver {:keys [x y]}] (scroll driver x y))) (defn scroll-by - "Scrolls the window by offset (relatively the current position)." + "Have `driver` scroll by `x` `y` relative pixel offset." ([driver x y] (js-execute driver "window.scrollBy(arguments[0], arguments[1]);" x y)) ([driver {:keys [x y]}] (scroll-by driver x y))) (defn scroll-query - "Scrolls to the first element found with a query. + "Have `driver` scroll to the element found by query `q`. + + See [[query]] for details on `q`. Invokes element's `.scrollIntoView()` method. Accepts extra `param` argument that might be either boolean or object for more control. - - See this page for details: - https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView - " + See [Mozilla's docs for values](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView)." ([driver q] (let [el (query driver q)] (js-execute driver "arguments[0].scrollIntoView();" (el->ref el)))) @@ -1754,19 +1864,18 @@ (js-execute driver "arguments[0].scrollIntoView(arguments[1]);" (el->ref el) param)))) (defn get-scroll - "Returns the current scroll position as a map - with `:x` and `:y` keys and integer values." + "Have `driver` return the current scroll position as a map of `:x` `:y`." [driver] (js-execute driver "return {x: window.scrollX, y: window.scrollY};")) (defn scroll-top - "Scrolls to top of the page keeping current horizontal position." + "Have `driver` scroll vertically to the top of the page keeping current horizontal position." [driver] (let [{:keys [x _y]} (get-scroll driver)] (scroll driver x 0))) (defn scroll-bottom - "Scrolls to bottom of the page keeping current horizontal position." + "Have `driver` scroll vertically to bottom of the page keeping current horizontal position." [driver] (let [y-max (js-execute driver "return document.body.scrollHeight;") {:keys [x _y]} (get-scroll driver)] @@ -1776,32 +1885,36 @@ scroll-offset 100) (defn scroll-up - "Scrolls the page up by specific number of pixels. - The `scroll-offset` constant is used when not passed." + "Have `driver` scroll the page up by `offset` pixels. + + `offset` defaults to [[scroll-offset]]." ([driver offset] (scroll-by driver 0 (- offset))) ([driver] (scroll-up driver scroll-offset))) (defn scroll-down - "Scrolls the page down by specific number of pixels. - The `scroll-offset` constant is used when not passed." + "Have `driver` scroll the page down by `offset` pixels. + + `offset` defaults to [[scroll-offset]]." ([driver offset] (scroll-by driver 0 offset)) ([driver] (scroll-down driver scroll-offset))) (defn scroll-left - "Scrolls the page left by specific number of pixels. - The `scroll-offset` constant is used when not passed." + "Have `driver` scroll the page left by `offset` pixels. + + `offset` defaults to [[scroll-offset]]." ([driver offset] (scroll-by driver (- offset) 0)) ([driver] (scroll-left driver scroll-offset))) (defn scroll-right - "Scrolls the page right by specific number of pixels. - The `scroll-offset` constant is used when not passed." + "Have `driver` scroll the page right by `offset` pixels. + + `offset` defaults to [[scroll-offset]]." ([driver offset] (scroll-by driver offset 0)) ([driver] @@ -1811,8 +1924,10 @@ ;; iframes ;; -(defn switch-frame* - "Switches to an (i)frame by its index or an element reference." +(defn ^:no-doc switch-frame* + "Have `driver` switch context to (i)frame by its `id` + + `id` can be an index or the element `id` attribute." [driver id] (execute {:driver driver :method :post @@ -1820,31 +1935,35 @@ :data {:id id}})) (defn switch-frame - "Switches to an (i)frame quering the page for it." + "Have `driver` switch context to (i)frame element found by query `q`. + + See [[query]] for details on `q`." [driver q] (let [el (query driver q)] (switch-frame* driver (el->ref el)))) (defn switch-frame-first - "Switches to the first (i)frame." + "Have `driver` switch context to the first (i)frame." [driver] (switch-frame* driver 0)) (defn switch-frame-parent - "Switches to the parent of the current (i)frame." + "Have `driver` switch context to the parent of the current (i)frame." [driver] (execute {:driver driver :method :post :path [:session (:session driver) :frame :parent]})) (defn switch-frame-top - "Switches to the most top of the page." + "Have `driver` switch context the main page." [driver] (switch-frame* driver nil)) (defmacro with-frame - "Switches to the (i)frame temporary while executing the body - returning the result of the last expression." + "Excecute `body` within context of frame found via `driver` by query `q`. + + Frame context is restored after call. + Result of `body` is returned." [driver q & body] `(do (switch-frame ~driver ~q) @@ -1857,7 +1976,7 @@ ;; (defmulti get-log-types - "Returns a set of log types the browser supports." + "Have `driver` return a vector of log types the browser supports." {:arglists '([driver])} dispatch-driver) @@ -1868,7 +1987,7 @@ :method :get :path [:session (:session driver) :log :types]}))) -(defn process-log +(defn ^:no-doc process-log "Remaps some of the log's fields." [entry] (-> entry @@ -1877,30 +1996,6 @@ (assoc :datetime (java.util.Date. ^long (:timestamp entry))))) (defmulti ^:private get-logs* - "Returns Javascript log entries. Each log entry is a map - with the following structure: - - {:level :warning, - :message \"1,2,3,4 anonymous (:1)\", - :timestamp 1511449388366, - :source nil, - :datetime #inst \"2017-11-23T15:03:08.366-00:00\"} - - Empirical knowledge about browser differences: - - * Chrome: - - Returns all recorded logs. - - Clears the logs once they have been read. - - JS console logs have `:console-api` for `:source` field. - - Entries about errors will have SEVERE level. - - * PhantomJS (obsolete and no longer tested): - - Return all recorded logs since the last URL change. - - Does not clear recorded logs on subsequent invocations. - - JS console logs have nil for `:source` field. - - Entries about errors will have WARNING level, as coded here: - https://github.com/detro/ghostdriver/blob/be7ffd9d47c1e76c7bfa1d47cdcde9164fd40db8/src/session.js#L494 - " {:arglists '([driver logtype])} dispatch-driver) @@ -1914,20 +2009,41 @@ :value (mapv process-log))) - (defn get-logs + "Have `driver` return Javascript console log entries. + + Each log entry is a map with the following structure: + ```Clojure + {:level :warning, + :message \"1,2,3,4 anonymous (:1)\", + :timestamp 1511449388366, + :source nil, + :datetime #inst \"2017-11-23T15:03:08.366-00:00\"} + ``` + + Empirical knowledge about browser differences: + + - Chrome: + - Returns all recorded logs. + - Clears the logs once they have been read. + - JS console logs have `:console-api` for `:source` field. + - Entries about errors will have SEVERE level. + + - PhantomJS (obsolete and no longer tested): + - Return all recorded logs since the last URL change. + - Does not clear recorded logs on subsequent invocations. + - JS console logs have nil for `:source` field. + - Entries about errors will have WARNING level, as coded [here](https://github.com/detro/ghostdriver/blob/be7ffd9d47c1e76c7bfa1d47cdcde9164fd40db8/src/session.js#L494)." ([driver] (get-logs driver "browser")) ([driver logtype] (get-logs* driver logtype))) - (defn supports-logs? - "Checks whether a driver supports getting console logs." + "Returns true if `driver` supports getting console logs." [driver] (implemented? driver get-logs*)) - (defn- dump-logs [logs filename & [opt]] (json/generate-stream @@ -1935,7 +2051,6 @@ (io/writer filename) (merge {:pretty true} opt))) - ;; ;; get/set hash ;; @@ -1944,56 +2059,85 @@ (str/split url #"#" 2)) (defn set-hash - "Sets a new hash fragment for the current page. - Don't include the leading # symbol. Useful when navigating - on single page applications." + "Have `driver` set a new `hash` fragment for the current page. + + Don't include the leading `#` character in `hash`. + + Useful when navigating on single page applications. See also: [[get-hash]]." [driver hash] (let [[url _] (split-hash (get-url driver)) new (format "%s#%s" url hash)] (go driver new))) (defn get-hash - "Returns the current hash fragment (nil when not set)." + "Have `driver` fetch the current hash fragment for the current page (nil when not set). + + Example: + ```Clojure + (def driver (chrome)) + (go driver \"https://en.wikipedia.org/wiki/Clojure\") + (get-hash driver) + ;; => nil + (go driver \"https://en.wikipedia.org/wiki/Clojure#Popularity\") + (get-hash driver) + ;; => \"Popularity\" + ```` + + See also: [[set-hash]]." [driver] (let [[_ hash] (split-hash (get-url driver))] hash)) - ;; ;; exceptions ;; -(defmacro with-exception [catch fallback & body] +(defmacro ^:private with-exception [catch fallback & body] `(try+ ~@body (catch ~catch ~(quote _) ~fallback))) -(defmacro with-http-error [& body] +(defmacro with-http-error + "Executes `body` suppressing catching any exception that would normally occur due to HTTP non-success status + when communicating with a WebDriver. + + Instead returns false on first HTTP non-success status." + [& body] `(with-exception [:type :etaoin/http-error] false ~@body)) ;; -;; locators TODO: drop or refactor +;; default locators ;; -(defn use-locator [driver locator] +(defn- use-locator + "Return `driver` with new default `locator`" + [driver locator] (assoc driver :locator locator)) -(defn use-xpath [driver] +(defn use-xpath + "Return new `driver` with default locator set to XPath." + [driver] (use-locator driver locator-xpath)) -(defn use-css [driver] +(defn use-css + "Return new `driver` with default locator set to CSS." + [driver] (use-locator driver locator-css)) -(defmacro with-locator [driver locator & body] +(defmacro ^:no-doc with-locator [driver locator & body] `(binding [~driver (assoc ~driver :locator ~locator)] ~@body)) -(defmacro with-xpath [driver & body] +(defmacro with-xpath + "Execute `body` with default locator set to XPath." + [driver & body] `(with-locator ~driver locator-xpath ~@body)) -(defmacro with-css [driver & body] +(defmacro with-css + "Execute `body` with default locator set to CSS." + [driver & body] `(with-locator ~driver locator-css ~@body)) @@ -2002,16 +2146,12 @@ ;; (defmulti get-alert-text - "Returns a string of text that appears in alert dialog (if present)." + "Have `driver` return text string from alert dialog (if present)." + {:arglists '([driver])} dispatch-driver) -(defmethod get-alert-text :firefox - [driver] - (:value (execute {:driver driver - :method :get - :path [:session (:session driver) :alert :text]}))) - -(defmethod get-alert-text :safari +(defmethods get-alert-text + [:firefox :safari] [driver] (:value (execute {:driver driver :method :get @@ -2025,16 +2165,12 @@ :path [:session (:session driver) :alert_text]}))) (defmulti dismiss-alert - "Simulates cancelling an alert dialog (pressing cross button)." + "Have `driver` cancel open alert dialog." + {:arglists '([driver])} dispatch-driver) -(defmethod dismiss-alert :firefox - [driver] - (execute {:driver driver - :method :post - :path [:session (:session driver) :alert :dismiss]})) - -(defmethod dismiss-alert :safari +(defmethods dismiss-alert + [:firefox :safari] [driver] (execute {:driver driver :method :post @@ -2048,16 +2184,12 @@ :path [:session (:session driver) :dismiss_alert]})) (defmulti accept-alert - "Simulates submitting an alert dialog (pressing OK button)." + "Have `driver` accept open alert dialog." + {:arglists '([driver])} dispatch-driver) -(defmethod accept-alert :firefox - [driver] - (execute {:driver driver - :method :post - :path [:session (:session driver) :alert :accept]})) - -(defmethod accept-alert :safari +(defmethods accept-alert + [:firefox :safari] [driver] (execute {:driver driver :method :post @@ -2075,7 +2207,7 @@ ;; (defn running? - "Check whether a driver runs HTTP server." + "Return true if `driver` seems accessable via its host and port." [driver] (util/connectable? (:host driver) (:port driver))) @@ -2084,52 +2216,57 @@ ;; predicates ;; -(defn driver? [driver type] +(defn driver? + "Return true if `driver` is of `type` (e.g. on of: `:chrome`, `:edge`, `:firefox`, `:safari`, `:phantom`)" + [driver type] (= (dispatch-driver driver) type)) (defn chrome? - "Returns true if a driver is a Chrome instance." + "Returns true if a `driver` is Chrome." [driver] (driver? driver :chrome)) (defn edge? - "Returns true if a driver is an Edge instance." + "Returns true if a `driver` is Edge." [driver] (driver? driver :edge)) (defn firefox? - "Returns true if a driver is a Firefox instance." + "Returns true if a `driver` is Firefox." [driver] (driver? driver :firefox)) (defn phantom? - "Returns true if a driver is a Phantom.js instance." + "Returns true if a `driver` is Phantom.js." [driver] (driver? driver :phantom)) (defn safari? - "Returns true if a driver is a Safari instance." + "Returns true if a `driver` is Safari." [driver] (driver? driver :safari)) (defn headless? - "Returns true if a driver is run in headless mode (without UI window)." + "Returns true if a `driver` is in headless mode (without UI window)." [driver] (drv/is-headless? driver)) (defn exists? - "Returns true if an element exists on the page. + "Return true if `driver` can find element via query `q`. + + See [[query]] for details on `q`. - Keep in mind it does not validates whether the element is visible, - clickable and so on." + Keep in mind this does not validate whether the found element is visible, clickable and so on." [driver q & more] (with-http-error (apply query driver q more) true)) -(def ^{:doc "Opposite of `exists?`."} +(def ^{:doc "Opposite of [[exists?]]." + :arglists '([driver q & more])} absent? (complement exists?)) +;; TODO: I think we might have broken this! @lread (defn- effectively-displayed? [driver el] {:pre [(some? el)]} @@ -2137,11 +2274,14 @@ (not= "hidden" (get-element-css-el driver el :visibility)))) (defmulti displayed-el? - "Returns true if an element is effectively displayed/visible. + "Return true if `driver` finds `el` is effectively displayed/visible. - Rather than checking the browser native `displayed` implementation, which - isn't 100% reliable, we are taking a pragmatic approach by checking the - `display` and `visibility` CSS properties." + See [[query]] for details on `q`. + + Rather than checking the browser native `displayed` implementation, which + isn't 100% reliable, we are taking a pragmatic approach by checking the + `display` and `visibility` CSS properties." + {:arglists '([driver el])} dispatch-driver) (defmethod displayed-el? :default @@ -2150,20 +2290,28 @@ (effectively-displayed? driver el)) (defn displayed? - "Checks whether an element is displayed an screen." + "Return true if element found by `driver` with query `q` is effectively displayed." [driver q] (displayed-el? driver (query driver q))) (defn visible? - "Checks whether an element is visible on the page." + "Return true if element found by `driver` with query `q` exists and is effectively displayed. + + See [[query]] for details on `q`. + + Same as [[displayed?]] but does not throw if element does not exist." [driver q] (and (exists? driver q) (displayed? driver q))) -(def ^{:doc "Oppsite to `visible?`."} +(def ^{:doc "Oppsite of [[visible?]]." + :arglists '([driver q])} invisible? (complement visible?)) (defn selected-el? + "Return true if `driver` determines element `el` is selected. + + For use on input elements like checkboxes, radio buttons and option elements." [driver el] {:pre [(some? el)]} (:value (execute {:driver driver @@ -2171,11 +2319,18 @@ :path [:session (:session driver) :element el :selected]}))) (defn selected? - "Checks whether an element is selected." + "Return true if `driver` determines element found by query `q` is selected. + + See [[query]] for details on `q`. + + For use on input elements like checkboxes, radio buttons and option elements." [driver q] (selected-el? driver (query driver q))) (defn enabled-el? + "Return true if `driver` determines element `el` is enabled. + + For use on form elements." [driver el] {:pre [(some? el)]} (:value (execute {:driver driver @@ -2183,16 +2338,23 @@ :path [:session (:session driver) :element el :enabled]}))) (defn enabled? - "Checks whether an element is enabled." + "Returns true if `driver` determines element found by query `q` is enabled. + + See [[query]] for details on `q`. + + For use on form elements." [driver q] (enabled-el? driver (query driver q))) -(def disabled? (complement enabled?)) +(def ^{:doc "Opposite of [[enabled?]]" + :arglists '([query q])} + disabled? (complement enabled?)) (defn has-text? - "Returns true if a passed text appears anywhere on a page. - With a leading query expression, finds a text inside the first - element that matches the query." + "Return true if `driver` finds that `text` appears anywhere on a page. + + When `q` is specified, restricts search inside the element that matches query `q`. + See [[query]] for details on `q`." ([driver text] (with-http-error (boolean @@ -2207,6 +2369,7 @@ (query driver q x))))))) (defn has-class-el? + "Returns true if `driver` finds that element `el` includes `class` in its class attribute." [driver el class] {:pre [(some? el)]} (let [classes (get-element-attr-el driver el "class")] @@ -2216,73 +2379,81 @@ (str/includes? classes (name class))))) (defn has-class? - "Checks whether an element has a specific class." + "Returns true if `driver` finds that element found by query `q` includes `class` in its class attribute. + + See [[query]] for details on `q`." [driver q class] (has-class-el? driver (query driver q) class)) -(def ^{:doc "Opposite to `has-class?`."} +(def ^{:doc "Opposite of [[has-class?]]." + :arglists '([query q class])} has-no-class? (complement has-class?)) (defn has-alert? - "Checks if there is an alert dialog opened on the page." + "Returns true if `driver` sees an open alert dialog." [driver] (with-http-error (get-alert-text driver) true)) -(def ^{:doc "Opposite to `has-alert?`."} +(def ^{:doc "Opposite of [[has-alert?]]." + :arglists '([driver])} has-no-alert? (complement has-alert?)) ;; ;; wait functions ;; -(def ^:dynamic *wait-timeout* 7) -(def ^:dynamic *wait-interval* 0.33) - +(def ^:dynamic *wait-timeout* "Maximum seconds to wait, default for `wait-*` functions." 7) +(def ^:dynamic *wait-interval* "Frequency in seconds to check if we should still wait, default for `wait-*` functions." 0.33) (defmacro with-wait-timeout - [sec & body] - `(binding [*wait-timeout* ~sec] + "Execute `body` with a [[*wait-timeout*]] of `seconds`" + [seconds & body] + `(binding [*wait-timeout* ~seconds] ~@body)) (defmacro with-wait-interval - [sec & body] - `(binding [*wait-interval* ~sec] + "Execute `body` with a [[*wait-interval*]] of `seconds` (which can be fractional)." + [seconds & body] + `(binding [*wait-interval* ~seconds] ~@body)) (defn wait - "Sleeps for N seconds." - (#_{:clj-kondo/ignore [:unused-binding]} [driver sec] - (wait sec)) - ([sec] - (Thread/sleep (* sec 1000)))) + "Sleep for `seconds`." + (#_{:clj-kondo/ignore [:unused-binding]} [driver seconds] + (wait seconds)) + ([seconds] + (Thread/sleep (* seconds 1000)))) (defmacro with-wait - "Executes the body waiting for n seconds before each form. - Returns a value of the last form. Use that macros to perform - a bunch of actions slowly. Some SPA applications need extra time + "Execute `body` waiting `seconds` before each form. + + Returns the value of the last form. + + Can be used to perform actions slowly. Some SPA applications need extra time to re-render the content." - [n & body] - `(do ~@(interleave (repeat `(wait ~n)) body))) + [seconds & body] + `(do ~@(interleave (repeat `(wait ~seconds)) body))) (defmacro doto-wait - "The same as doto but prepends each form with (wait n) clause." - [n obj & body] + "The same as `clojure.core/doto` but prepends each form with ([[wait]] `seconds`) clause." + [seconds obj & body] `(doto ~obj - ~@(interleave (repeat `(wait ~n)) body))) + ~@(interleave (repeat `(wait ~seconds)) body))) (defn wait-predicate - "Sleeps continuously calling a predicate until it returns true. - Raises a slingshot exception when timeout is reached. + "Wakes up every `:interval` seconds to call `pred`. + Keeps this up until either `pred` returns true or `:timeout` has elapsed. + When `:timeout` has elapsed a slingshot exception is throws with `:message`. Arguments: - - `pred`: a zero-argument predicate to call; + - `pred`: a zero-argument predicate to call - `opt`: a map of optional parameters: - -- `:timeout` wait limit in seconds, 20 by default; - -- `:interval` how long to wait b/w calls, 0.33 by default; - -- `:message` a message that becomes a part of exception when timeout is reached." + - `:timeout` wait limit in seconds, [[*wait-timeout*]] by default; + - `:interval` how long to wait between calls, [[*wait-interval*]] by default; + - `:message` a message that becomes a part of exception when timeout is reached." ([pred] (wait-predicate pred {})) @@ -2308,13 +2479,9 @@ :times (inc times))))))) (defn wait-exists - "Waits until an element exists on a page (but may not be visible though). + "Waits until `driver` finds element [[exists?]] via `q`. - Arguments: - - - `driver`: a driver instance; - - `q`: a query term (see `query`); - - `opt`: a map of options (see `wait-predicate`)." + - `opt`: see [[wait-predicate]] opt." [driver q & [opt]] (let [message (format "Wait until %s element exists" q)] @@ -2322,13 +2489,11 @@ (merge {:message message} opt)))) (defn wait-absent - "Waits until an element is absent. + "Waits until `driver` determines element is not found by `q` (is [[absent?]]). - Arguments: + See [[query]] for details on `q`. - - `driver`: a driver instance; - - `q`: a query term (see `query`); - - `opt`: a map of options (see `wait-predicate`)." + - `opt`: see [[wait-predicate]] opt." [driver q & [opt]] (let [message (format "Wait until %s element is absent" q)] @@ -2336,83 +2501,64 @@ (merge {:message message} opt)))) (defn wait-visible - "Waits until an element presents and is visible. - - Arguments: + "Waits until `driver` determines element found by `q` is [[visible?]]. - - `driver`: a driver instance; - - `q`: a query term (see `query`); - - `opt`: a map of options (see `wait-predicate`)." + See [[query]] for details on `q`. + - `opt`: see [[wait-predicate]] opt." [driver q & [opt]] (let [message (format "Wait until %s element is visible" q)] (wait-predicate #(visible? driver q) (merge {:message message} opt)))) (defn wait-invisible - "Waits until an element presents but not visible. + "Waits until `driver` determines element found by `q` is [[invisible?]]. - Arguments: - - - `driver`: a driver instance; - - `q`: a query term (see `query`); - - `opt`: a map of options (see `wait-predicate`)." + See [[query]] for details on `q`. + - `opt`: see [[wait-predicate]] opt." [driver q & [opt]] (let [message (format "Wait until %s element is invisible" q)] (wait-predicate #(invisible? driver q) (merge {:message message} opt)))) (defn wait-enabled - "Waits until an element is enabled (usually an input element). - - Arguments: + "Waits until `driver` determines element found by `q` is [[enabled?]]. - - `driver`: a driver instance; - - `q`: a query term (see `query`); - - `opt`: a map of options (see `wait-predicate`)." + See [[query]] for details on `q`. + - `opt`: see [[wait-predicate]] opt." [driver q & [opt]] (let [message (format "Wait until %s element is enabled" q)] - (wait-predicate #(enabled? driver q) + (wait-predicate #(enabled? driver q) (merge {:message message} opt)))) (defn wait-disabled - "Waits until an element is disabled (usually an input element). - - Arguments: + "Waits until `driver` determines element found by `q` is [[disabled?]]. - - `driver`: a driver instance; - - `q`: a query term (see `query`); - - `opt`: a map of options (see `wait-predicate`)." + See [[query]] for details on `q`. + - `opt`: see [[wait-predicate]] opt." [driver q & [opt]] (let [message (format "Wait until %s element is disabled" q)] - (wait-predicate #(disabled? driver q) + (wait-predicate #(disabled? driver q) (merge {:message message} opt)))) (defn wait-has-alert - "Waits until an alert dialog appears on the screen. - - Arguments: - - - `driver`: a driver instance; - - `opt`: a map of options (see `wait-predicate`)." + "Waits until `driver` finds page [[has-alert?]]. + - `opt`: see [[wait-predicate]] opt." [driver & [opt]] (let [message "Wait until element has alert"] - (wait-predicate #(has-alert? driver) + (wait-predicate #(has-alert? driver) (merge {:message message} opt)))) (defn wait-has-text - "Waits until an element has text anywhere inside it (including inner HTML). + "Waits until `driver` finds element via `q` with `text` anywhere inside it (including inner HTML). - Arguments: + See [[query]] for details on `q`. - - `driver`: a driver instance; - - `q`: a query term (see `query`). - - `text`: a string to search; - - `opt`: a map of options (see `wait-predicate`)." + - `opt`: see [[wait-predicate]] opt." [driver q text & [opt]] (let [message (format "Wait until %s element has text %s" q text)] @@ -2420,34 +2566,30 @@ (merge {:message message} opt)))) (defn wait-has-text-everywhere - "Like `wait-has-text` but searches for text across the entire page. + "Waits until `driver` finds `text` anywhere on the current page. - Arguments: - - - `driver`: a driver instance; - - `text`: a string to search; - - `opt`: a map of options (see `wait-predicate`)." + - `opt`: see [[wait-predicate]] opt." [driver text & [opt]] (let [q {:xpath "*"}] (wait-has-text driver q text opt))) (defn wait-has-class - "Waits until an element has specific class. - - Arguments: + "Waits until `driver` finds element via `q` [[has-class?]] `class`. - - `driver`: a driver instance; - - `q`: a query term (see `query`); - - `class`: a class to search as string; - - `opt`: a map of options (see `wait-predicate`)." + See [[query]] for details on `q`. + - `opt`: see [[wait-predicate]] opt." [driver q class & [opt]] (let [message (format "Wait until %s element has class %s" q class)] - (wait-predicate #(has-class? driver q class) + (wait-predicate #(has-class? driver q class) (merge {:message message} opt)))) -(defn wait-running [driver & [opt]] +(defn wait-running + "Waits until `driver` is [[running?]]. + + - `opt`: see [[wait-predicate]] opt." + [driver & [opt]] (log/debugf "Waiting for %s:%s is running" (:host driver) (:port driver)) (wait-predicate #(running? driver) opt)) @@ -2457,14 +2599,9 @@ ;; (defn click-visible - "Waits until an element becomes visible, then clicks on it. - - Arguments: - - - `driver`: a driver instance; - - `q`: a query term (see `query`); - - `opt`: a map of options (see `wait-predicate`)." + "Waits until `driver` finds visible element via query `q` then clicks on it. + - `opt`: see [[wait-predicate]] opt." [driver q & [opt]] (doto driver (wait-visible q opt) @@ -2474,7 +2611,12 @@ ;; touch ;; -(defmulti touch-tap dispatch-driver) +(defmulti touch-tap + "Have `driver` touch tap element found by query `q`. + + See [[query]] for details on `q`." + {:arglists '([driver q])} + dispatch-driver) (defmethod touch-tap :chrome @@ -2484,7 +2626,14 @@ :path [:session (:session driver) :touch :click] :data {:element (query driver q)}})) -(defmulti touch-down dispatch-driver) +(defmulti touch-down + "Have `driver` touch down + - on element found by query `q` + - or `x` `y` coordinate + + See [[query]] for details on `q`." + {:arglists '([driver q] [driver x y])} + dispatch-driver) (defmethod touch-down :chrome @@ -2498,7 +2647,14 @@ :path [:session (:session driver) :touch :down] :data {:x (int x) :y (int y)}}))) -(defmulti touch-up dispatch-driver) +(defmulti touch-up + "Have `driver` touch up + - on element found by query `q` + - or `x` `y` coordinate + + See [[query]] for details on `q`." + {:arglists '([driver q] [driver x y])} + dispatch-driver) (defmethod touch-up :chrome @@ -2512,7 +2668,14 @@ :path [:session (:session driver) :touch :up] :data {:x (int x) :y (int y)}}))) -(defmulti touch-move dispatch-driver) +(defmulti touch-move + "Have `driver` touch move + - on element found by query `q` + - or `x` `y` coordinate + + See [[query]] for details on `q`." + {:arglists '([driver q] [driver x y])} + dispatch-driver) (defmethod touch-move :chrome @@ -2530,85 +2693,80 @@ ;; skip/when driver ;; -(defmacro when-not-predicate [predicate & body] +(defmacro when-not-predicate + "Executes `body` when `predicate` returns falsy." + [predicate & body] `(when-not (~predicate) ~@body)) (defmacro when-not-drivers - "Executes the body only if a browsers is NOT in set #{:browser1 :browser2}" + "Executes `body` when browser `driver` is NOT in `browsers`, ex: `#{:chrome :safari}`" [browsers driver & body] `(when-not-predicate #((set ~browsers) (dispatch-driver ~driver)) ~@body)) (defmacro when-not-chrome - "Executes the body only if a browser is NOT Chrome." + "Executes `body` when browser `driver` is NOT Chrome." [driver & body] `(when-not-predicate #(chrome? ~driver) ~@body)) (defmacro when-not-edge - "Executes the body only if a browser is NOT Edge." + "Executes `body` when browser `driver` is NOT Edge." [driver & body] `(when-not-predicate #(edge? ~driver) ~@body)) (defmacro when-not-phantom - "Executes the body only if a browser is NOT Phantom.js." + "Executes `body` when browser `driver` is NOT Phantom.js." [driver & body] `(when-not-predicate #(phantom? ~driver) ~@body)) (defmacro when-not-firefox - "Executes the body only if a browser is NOT Firefox." + "Executes `body` when browser `driver` is NOT Firefox." [driver & body] `(when-not-predicate #(firefox? ~driver) ~@body)) (defmacro when-not-safari - "Executes the body only if a browser is NOT Safari." + "Executes `body` when browser `driver` is NOT Safari." [driver & body] `(when-not-predicate #(safari? ~driver) ~@body)) (defmacro when-not-headless - "Executes the body only if a browser is NOT run in headless mode." + "Executes `body` when browser `driver` is NOT in headless mode." [driver & body] `(when-not-predicate #(headless? ~driver) ~@body)) (defmacro when-predicate - "Executes the body only if a predicate returns true." + "Executes `body` when `predicate` returns truthy." [predicate & body] `(when (~predicate) ~@body)) (defmacro when-chrome - "Executes the body only if the driver is Chrome. - - Example: - - (def driver (chrome)) - (when-chrome driver - (println \"It's Chrome!\")" - + "Executes `body` when browser `driver` is Chrome." [driver & body] `(when-predicate #(chrome? ~driver) ~@body)) (defmacro when-phantom - "Executes the body only if the driver is Phantom.js." + "Executes `body` when browser `driver` is Phantom.js." [driver & body] `(when-predicate #(phantom? ~driver) ~@body)) (defmacro when-firefox - "Executes the body only if the driver is Firefox." + "Executes `body` when browser `driver` is Firefox." [driver & body] `(when-predicate #(firefox? ~driver) ~@body)) (defmacro when-edge - "Executes the body only if the driver is Edge." + "Executes `body` when browser `driver` is Edge." [driver & body] `(when-predicate #(edge? ~driver) ~@body)) (defmacro when-safari - "Executes the body only if the driver is Safari." + "Executes `body` when browser `driver` is Safari." [driver & body] `(when-predicate #(safari? ~driver) ~@body)) (defmacro when-headless - "Executes the body only if the driver is run in headless mode." + "Executes `body` when the `driver` is in headless mode." [driver & body] `(when-predicate #(headless? ~driver) ~@body)) @@ -2620,7 +2778,7 @@ (mapv str (apply str text more))) (defmulti fill-el - "Fills an element with text by its identifier." + "Have `driver` fill input element `el` with `text` (and optionally `more` text)." {:arglists '([driver el text & more])} dispatch-driver) @@ -2633,17 +2791,8 @@ :path [:session (:session driver) :element el :value] :data {:value (apply make-input* text more)}})) -(defmethod fill-el - :firefox ;; todo support the old version for :default - [driver el text & more] - {:pre [(some? el)]} - (execute {:driver driver - :method :post - :path [:session (:session driver) :element el :value] - :data {:text (str/join (apply make-input* text more))}})) - -(defmethod fill-el - :safari +(defmethods fill-el + [:firefox :safari] [driver el text & more] {:pre [(some? el)]} (execute {:driver driver @@ -2663,41 +2812,38 @@ :path [:session (:session driver) :keys] :data {:value (apply make-input* text more)}})) -(defmethod fill-active* - :firefox - [driver text & more] - (let [el (get-active-element* driver)] - (apply fill-el driver el text more))) - -(defmethod fill-active* - :safari +(defmethods fill-active* + [:firefox :safari] [driver text & more] (let [el (get-active-element* driver)] (apply fill-el driver el text more))) (defn fill-active - "Fills an active element with keys." + "Have `driver` fill active element with `text` (and optionally `more` text)." [driver text & more] (apply fill-active* driver text more)) (defn fill - "Fills an element found with a query with a given text. + "Have `driver` fill input element found by `q` with `text` (and optionally `more` text). - 0.1.6: now the rest parameters are supported. They will - joined using \"str\": + See [[query]] for details on `q`. + Example: + ```Clojure (fill driver :simple-input \"foo\" \"baz\" 1) - ;; fills the input with \"foobaz1\"" + ;; fills the input with text: foobaz1 + ```" [driver q text & more] (apply fill-el driver (query driver q) text more)) (defn fill-multi - "Fills multiple inputs in batch. + "Have `driver` fill multiple inputs via `q-text`. - `q-text` could be: + `q-text` can be: + - a map of `{q1 \"text1\" q2 \"text2\" ...}` + - a vector of `[q1 \"text1\" q2 \"text2\" ...]` - - a map of {query -> text} - - a vector of [query1 text1 query2 text2 ...]" + See [[query]] for details on `q`s." [driver q-text] (cond (map? q-text) @@ -2712,6 +2858,11 @@ :arg q-text}))) (defn fill-human-el + "Have `driver` fill element `el` with `text` as if it were a real human using `opt`. + + `opt` + - `:mistake-prob` probability of making a typo (0 to 1.0) (default: `0.1`) + - `:pause-max` maximum amount of time in seconds to pause between keystrokes (can be fractional) (default: `0.2`)" [driver el text opt] {:pre [(some? el)]} (let [{:keys [mistake-prob pause-max] @@ -2732,21 +2883,30 @@ (wait-key)))) (defn fill-human - "Fills text like humans do: with error, corrections and pauses. - - Arguments: - - - `driver`: a driver instance, + "Have `driver` fill element found by `q` with `text` as if it were a real human using `opt`. - - `q`: a query term, see `query` function for more info, + See [[query]] for details on `q`. - - `text`: a string to input." + `opt` + - `:mistake-prob` probability of making a typo (0 to 1.0) (default: `0.1`) + - `:pause-max` maximum amount of time in seconds to pause between keystrokes (can be fractional) (default: `0.2`)" ([driver q text] (fill-human driver q text {})) ([driver q text opt] (fill-human-el driver (query driver q) text opt))) (defn fill-human-multi - "`fill-human` + `fill-multi`" + "Have `driver` fill multiple elements as if it were a real human being via `q-text` using `opt`. + + `q-text` can be: + - a map of `{q1 \"text1\" q2 \"text2\" ...}` + - a vector of `[q1 \"text1\" q2 \"text2\" ...]` + + + See [[query]] for details on `q`s. + + `opt` + - `:mistake-prob` probability of making a typo (0 to 1.0) (default: `0.1`) + - `:pause-max` maximum amount of time in seconds to pause between keystrokes (can be fractional) (default: `0.2`)" ([driver q-text] (fill-human-multi driver q-text {})) ([driver q-text opt] (cond @@ -2762,16 +2922,10 @@ :arg q-text})))) (defn select - "Select option in select-box by visible text on click. + "Have `driver` select option with matching `text` for select element found by query `q`. The select element is clicked, then the option. - Arguments: - - - `driver`: a driver instance, - - - `q`: a query term to find the select box, see [[query]], - - - `text`: a string, text in the option you want to select" + See [[query]] for details on `q`." [driver q text] (let [select-el (query driver q)] (click-el driver select-el) @@ -2779,7 +2933,7 @@ (click-el driver option-el)))) (defn clear-el - "Clears an element by its identifier." + "Have `driver` clear input element `el`" [driver el] {:pre [(some? el)]} (execute {:driver driver @@ -2787,11 +2941,11 @@ :path [:session (:session driver) :element el :clear]})) (defn clear - "Clears an element (input, textarea) found with a query. + "Have `driver` clear input element found by `q` (and optionally `more-qs`). - 0.1.6: multiple queries added." - [driver q & more] - (doseq [q (cons q more)] + See [[query]] for details on `q`." + [driver q & more-qs] + (doseq [q (cons q more-qs)] (clear-el driver (query driver q)))) ;; @@ -2799,16 +2953,15 @@ ;; (defmulti upload-file - "Attaches a local file to a file input field. + "Have `driver` attach a local file `path` to a file input field found by query `q`. Arguments: - - `q` is a query term that refers to a file input; - - `file` is either a string or java.io.File object - that references a local file. The file should exist. + - `q` see [[query]] for details; + - `file` is either a string or java.io.File object that references a local file. The file should exist. - Under the hood, it sends the file's name as a sequence of keys - to the input." + Under the hood, we send the file's name as a sequence of keys to the input." + {:arglists '([driver q path])} (fn [_driver _q file] (type file))) @@ -2831,7 +2984,9 @@ ;; (defn submit - "Sends Enter button value to an element found with query." + "Have `driver` submit a form by sending the enter key to input element found by query `q`. + + See [[query]] for details on `q`." [driver q] (fill driver q k/enter)) @@ -2862,18 +3017,18 @@ :data {:type type :ms (util/sec->ms sec)}})) (defmulti set-script-timeout - "Sets timeout for executing JS sctipts." - {:arglists '([driver sec])} + "Sets `driver` timeout `seconds` for executing JavaScript." + {:arglists '([driver seconds])} dispatch-driver) (defmethod set-script-timeout :default - [driver sec] - (set-timeout* driver :script sec)) + [driver seconds] + (set-timeout* driver :script seconds)) (defmulti set-page-load-timeout - "Sets timeout for loading pages." - {:arglists '([driver sec])} + "Sets `driver` timeout `seconds` for loading a page." + {:arglists '([driver seconds])} dispatch-driver) (defmethod set-page-load-timeout @@ -2883,18 +3038,18 @@ (defmethod set-page-load-timeout :chrome - [driver sec] - (set-timeout* driver "page load" sec)) + [driver seconds] + (set-timeout* driver "page load" seconds)) (defmulti set-implicit-timeout - "Sets timeout that is used when finding elements on the page." - {:arglists '([driver sec])} + "Sets `driver` timeout `seconds` to find elements on the page." + {:arglists '([driver seconds])} dispatch-driver) (defmethod set-implicit-timeout :default - [driver sec] - (set-timeout* driver :implicit sec)) + [driver seconds] + (set-timeout* driver :implicit seconds)) (defmulti ^:private get-timeout* "Basic method to get a map of all the timeouts." @@ -2909,26 +3064,26 @@ :path [:session (:session driver) :timeouts]}))) (defn get-script-timeout - "Returns the current script timeout in seconds." + "Returns `driver` timeout in seconds for executing JavaScript." [driver] (-> driver get-timeout* :script util/ms->sec)) (defn get-page-load-timeout - "Returns the current page load timeout in seconds." + "Returns `driver` timeout in seconds for loading a page." [driver] (-> driver get-timeout* :pageLoad util/ms->sec)) (defn get-implicit-timeout - "Returns the current implicit timeout in seconds." + "Returns `driver` timeout in seconds to find elements on the page." [driver] (-> driver get-timeout* :implicit util/ms->sec)) (defmacro with-script-timeout - "Performs the body setting the script timeout temporary. - Useful for async JS scripts." - [driver sec & body] + "Execute `body` temporarily setting `driver` to timeout `seconds` for executing JavaScript. + Useful for asynchronous scripts." + [driver seconds & body] `(let [prev# (get-script-timeout ~driver)] - (set-script-timeout ~driver ~sec) + (set-script-timeout ~driver ~seconds) (try ~@body (finally @@ -2960,15 +3115,10 @@ (.write out ^bytes (b64-decode b64str)))) (defmulti screenshot - "Takes a screenshot of the current page. Saves it in a *.png file on disk. - Rises exception if a screenshot was empty. - - Arguments: + "Have `driver` save a PNG format screenshot of the current page to `file`. + Throws if screenshot is empty. - - `driver`: driver instance, - - - `file`: either a path to a file or a native `java.io.File` instance. - " + `file` can be either a string or `java.io.File`." {:arglists '([driver file])} dispatch-driver) @@ -2982,15 +3132,19 @@ (b64-to-file b64str file) (util/error "Empty screenshot")))) -;; TODO add w3c screenshot (defmulti screenshot-element + "Have `driver` save a PNG format screenshot of the element found by query `q` to `file`. + + See [[query]] for details on `q`. + + `file` can be either a string or `java.io.File`." {:arglists '([driver q file])} dispatch-driver) (defmethod screenshot-element :default [_driver _q _file] - (util/error "This driver doesn't support screening elements.")) + (util/error "This driver doesn't support screenshots on elements.")) (defmethods screenshot-element [:chrome :edge :firefox] @@ -3004,7 +3158,7 @@ (b64-to-file b64str file) (util/error "Empty screenshot, query: %s" q)))) -(defn make-screenshot-file-path +(defn ^:no-doc make-screenshot-file-path [driver-type dir] (->> (.getTime (java.util.Date.)) (format "-%d.png") @@ -3013,7 +3167,9 @@ str)) (defmacro with-screenshots - "Makes a screenshot after each form" + "Have `driver` save a PNG imge screenshot to `dir` after each form in `body` is executed. + + Filenames will contain an epoch timestamp." [driver dir & body] (let [screenshot-form# `(screenshot ~driver (make-screenshot-file-path (:type ~driver) ~dir)) new-body (interleave body (repeat screenshot-form#))] @@ -3023,19 +3179,19 @@ ;; postmortem ;; -(defn get-pwd [] +(defn- get-pwd [] (System/getProperty "user.dir")) -(defn join-path +(defn- join-path "Joins two and more path components into a single file path OS-wisely." [p1 p2 & more] (.getPath ^java.io.File (apply fs/file p1 p2 more))) -(defn format-date +(defn- format-date [date pattern] (.format (SimpleDateFormat. pattern) date)) -(defn postmortem-handler +(defn ^:no-doc postmortem-handler "Internal postmortem handler that creates files. See the `with-postmortem`'s docstring below for more info." [driver {:keys [dir dir-src dir-img dir-log date-format]}] @@ -3075,37 +3231,23 @@ (dump-logs (get-logs driver) path-log)))) (defmacro with-postmortem - "Wraps the body with postmortem handler. If any error occurs, - it will save a screenshot, the page's source code and console logs - (if supported) on disk before rising an exception. Having them - could help you to discover what happened. - - Note: do not use it in test's fixtures. The standard `clojure.test` - framework has its own way of handling exceptions, so wrapping a fixture - with `(with-postmortem...)` would be in vain. + "Executes `body` with postmortem handling. + Good for forensics. - Arguments: + If an exception occurs, saves: + - screenshot `.png` to `:dir-img` else `:dir` else current working directory + - page source `.html` to `:dir-scr` else `:dir` else current working directory + - console log `.json` to `:dir-log` else `:dir` else current working directory - - `driver`: a driver instance, + Dirs are automatically created if necessary. - - `opt`: a map of options, where: + `:date-format` used in filename to keep them unique. + See Java SDK `SimpleDateFormat` for available patterns. + Defaults to `\"yyyy-MM-dd-HH-mm-ss\"`. - -- `:dir` path to a directory where to store artifacts by default. - Might not exist, will be created otherwise. When not passed, - the current working directory (`pwd`) is used. - - -- `:dir-img`: path to a directory where to store `.png` - files (screenshots). If `nil`, `:dir` value is used. - - -- `:dir-src`: path to a directory where to store `.html` - files (page source). If `nil`, `:dir` value is used. - - -- `:dir-log`: path to a directory where to store `.json` - files with console logs. If `nil`, `:dir` value is used. - - -- `:date-format`: a string represents date(time) pattern to make - filenames unique. Default is \"yyyy-MM-dd-HH-mm-ss\". See Oracle - Java `SimpleDateFormat` class manual for more patterns." + Tip: don't bother using `with-postmortem` in test fixtures. + The standard `clojure.test`framework has its own way of handling exceptions, + so wrapping a fixture with `(with-postmortem...)` would be in vain." [driver opt & body] `(try ~@body @@ -3117,8 +3259,8 @@ ;; driver management ;; -(defn make-url - "Makes an Webdriver URL from a host and port." +(defn- make-url + "Returns a WebDriver url string for `host` and `port`." [host port] (format "http://%s:%s" host port)) @@ -3166,8 +3308,7 @@ (log/debugf "Created driver: %s %s:%s" (name type) host port) driver)) - -(defn proxy-env +(defn- proxy-env [proxy] (let [http (System/getenv "HTTP_PROXY") ssl (System/getenv "HTTPS_PROXY")] @@ -3337,10 +3478,10 @@ (assoc driver :session session))) (defn disconnect-driver - "Disconnects from a running Webdriver server. + "Returns new `driver` after disconnecting from a running WebDriver process. Closes the current session that is stored in the driver if it still exists. - Removes the session from the driver instance. Returns modified driver." + Removes the session from `driver`." [driver] (try (delete-session driver) @@ -3352,22 +3493,20 @@ (dissoc driver :session)) (defn stop-driver - "Stops the driver's process. Removes proces's data from the driver - instance. Returns a modified driver." + "Returns new `driver` after killing its WebDriver process." [driver] (proc/kill (:process driver)) (dissoc driver :process :args :env :capabilities)) (defn boot-driver - "Three-in-one: creates a driver, starts a process and creates a new - session. Returns the driver instance. - - Arguments: + "Launch and return a driver of `type` (e.g. `:chrome`, `:firefox` `:safari` `:edge` `:phantom`) + with `opt` options. - - `type` a keyword determines a driver type. + - creates a driver + - launches a WebDriver process (or connects to an existing running process if `:host` is specified) + - creates a session for driver - - `opt` a map of all possible parameters that `-create-driver`, - `-run-driver` and `-connect-driver` may accept." + `opt` is optionally, see [Driver Options](/doc/01-user-guide.adoc#driver-options)." ([type] (boot-driver type {})) ([type {:keys [host] :as opt}] @@ -3377,7 +3516,7 @@ true (-connect-driver opt)))) (defn quit - "Closes the current session and stops the driver." + "Have `driver` close the current session, then, if Etaoin launched it, kill the WebDriver process." [driver] (let [process (:process driver)] (try @@ -3386,69 +3525,77 @@ (when process (stop-driver driver)))))) -(def firefox - "Launches Firefox driver. A shortcut for `boot-driver`." +(def ^{:arglists '([] [opt])} firefox + "Launch and return a Firefox driver. + + `opt` is optionally, see [Driver Options](/doc/01-user-guide.adoc#driver-options)." (partial boot-driver :firefox)) -(def edge - "Launches Edge driver. A shortcut for `boot-driver`." +(def ^{:arglists '([] [opt])} edge + "Launch and return an Edge driver. + + `opt` is optionally, see [Driver Options](/doc/01-user-guide.adoc#driver-options)." (partial boot-driver :edge)) -(def chrome - "Launches Chrome driver. A shortcut for `boot-driver`." +(def ^{:arglists '([] [opt])} chrome + "Launch and return a Chrome driver. + + `opt` is optionally, see [Driver Options](/doc/01-user-guide.adoc#driver-options)." (partial boot-driver :chrome)) -(def phantom - "Launches Phantom.js driver. A shortcut for `boot-driver`." +(def ^{:arglists '([] [opt])} phantom + "Launch and return a Phantom.js driver. + + `opt` is optionally, see [Driver Options](/doc/01-user-guide.adoc#driver-options)." (partial boot-driver :phantom)) -(def safari - "Launches Safari driver. A shortcut for `boot-driver`." +(def ^{:arglists '([] [opt])} safari + "Launch and return a Safari driver. + + `opt` is optionally, see [Driver Options](/doc/01-user-guide.adoc#driver-options)." (partial boot-driver :safari)) (defn chrome-headless - "Launches headless Chrome driver. A shortcut for `boot-driver`." + "Launch and return a headless Chrome driver. + + `opt` is optionally, see [Driver Options](/doc/01-user-guide.adoc#driver-options)." ([] (chrome-headless {})) ([opt] (boot-driver :chrome (assoc opt :headless true)))) (defn firefox-headless - "Launches headless Firefox driver. A shortcut for `boot-driver`." + "Launch and return a headless Firefox driver. + + `opt` is optionally, see [Driver Options](/doc/01-user-guide.adoc#driver-options)." ([] (firefox-headless {})) ([opt] (boot-driver :firefox (assoc opt :headless true)))) (defn edge-headless - "Launches headless Edge driver. A shortcut for `boot-driver`." + "Launch and return a headless Edge driver. + + `opt` is optionally, see [Driver Options](/doc/01-user-guide.adoc#driver-options)." ([] (edge-headless {})) ([opt] (boot-driver :edge (assoc opt :headless true)))) (defmacro with-driver - "Performs the body within a driver session. + "Executes `body` with a driver session of `type` (e.g. `:chrome`, `:firefox` `:safari` `:edge` `:phantom`) + with `opt` options, binding driver instance to `bind`. - Launches a driver of a given type. Binds the driver instance to a - passed `bind` symbol. Executes the body once the driver has - started. Shutdowns the driver finally (even if an exception - occurred). + Driver is automatically launched and terminated (even if an exception occurs). - Arguments: - - - `type` is a keyword what driver type to start. - - - `opt` is a map with driver's options. See `boot-driver` for more - details. - - - `bind` is a symbol to bind a driver reference. + `opt` - can be `{}` or `nil`, see [Driver Options](/doc/01-user-guide.adoc#driver-options). Example: + ```Clojure (with-driver :firefox {} driver - (go driver \"http://example.com\")) - " + (go driver \"https://clojure.org\")) + ```" [type opt bind & body] `(client/with-pool {} (let [~bind (boot-driver ~type ~opt)] @@ -3458,57 +3605,137 @@ (quit ~bind)))))) (defmacro with-firefox - "Performs the body with Firefox session. A shortcut for - `with-driver`." + "Executes `body` with a Firefox driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `opt` - can be `{}` or `nil`, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-firefox {} driver + (go driver \"https://clojure.org\")) + ```" [opt bind & body] `(with-driver :firefox ~opt ~bind ~@body)) (defmacro with-chrome - "Performs the body with Chrome session. A shortcut for - `with-driver`." + "Executes `body` with a Chrome driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `opt` - can be `{}` or `nil`, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-chrome {} driver + (go driver \"https://clojure.org\")) + ```" [opt bind & body] `(with-driver :chrome ~opt ~bind ~@body)) (defmacro with-edge - "Performs the body with Edge session. A shortcut for - `with-driver`." + "Executes `body` with an Edge driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `opt` - can be `{}` or `nil`, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-edge {} driver + (go driver \"https://clojure.org\")) + ```" [opt bind & body] `(with-driver :edge ~opt ~bind ~@body)) (defmacro with-phantom - "Performs the body with Phantom.js session. A shortcut for - `with-driver`." + "Executes `body` with an Phantom.JS driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `opt` - can be `{}` or `nil`, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-phantom {} driver + (go driver \"https://clojure.org\")) + ```" [opt bind & body] `(with-driver :phantom ~opt ~bind ~@body)) (defmacro with-safari - "Performs the body with Safari session. A shortcut for - `with-driver`." + "Executes `body` with a Safari driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `opt` - can be `{}` or `nil`, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-safari {} driver + (go driver \"https://clojure.org\")) + ```" [opt bind & body] `(with-driver :safari ~opt ~bind ~@body)) (defmacro with-chrome-headless - "Performs the body with headless Chrome session. A shortcut for - `with-driver`." + "Executes `body` with a headless Chrome driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `opt` - can be `{}` or `nil`, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-chrome-headless {} driver + (go driver \"https://clojure.org\")) + ```" [opt bind & body] `(with-driver :chrome (assoc ~opt :headless true) ~bind ~@body)) (defmacro with-firefox-headless - "Performs the body with headless Firefox session. A shortcut for - `with-driver`." + "Executes `body` with a headless Firefox driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `opt` - can be `{}` or `nil`, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-firefox-headless {} driver + (go driver \"https://clojure.org\")) + ```" [opt bind & body] `(with-driver :firefox (assoc ~opt :headless true) ~bind ~@body)) (defmacro with-edge-headless - "Performs the body with headless Edge session. A shortcut for - `with-driver`." + "Executes `body` with a headless Edge driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `opt` - can be `{}` or `nil`, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-edge-headless {} driver + (go driver \"https://clojure.org\")) + ```" [opt bind & body] `(with-driver :edge (assoc ~opt :headless true) ~bind ~@body)) diff --git a/src/etaoin/api2.clj b/src/etaoin/api2.clj index 8dc3d6bf..7c83ef2d 100644 --- a/src/etaoin/api2.clj +++ b/src/etaoin/api2.clj @@ -1,52 +1,145 @@ (ns etaoin.api2 - " - Better syntax for some API that cannot be fixed - without breaking them. - " + "Improved syntax for some [[etaoin.api]] calls" (:require [etaoin.api :as e])) (defmacro with-firefox + "Executes `body` with a Firefox driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `options` - optional, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-firefox [driver] + (go driver \"https://clojure.org\")) + ```" [[bind & [options]] & body] `(e/with-driver :firefox ~options ~bind ~@body)) (defmacro with-chrome + "Executes `body` with a Chrome driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `options` - optional, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-chrome [driver] + (go driver \"https://clojure.org\")) + ```" [[bind & [options]] & body] `(e/with-driver :chrome ~options ~bind ~@body)) (defmacro with-edge + "Executes `body` with a Edge driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `options` - optional, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-edge [driver] + (go driver \"https://clojure.org\")) + ```" [[bind & [options]] & body] `(e/with-driver :edge ~options ~bind ~@body)) (defmacro with-phantom + "Executes `body` with a Phantom.JS driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `options` - optional, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-phantom [driver] + (go driver \"https://clojure.org\")) + ```" [[bind & [options]] & body] `(e/with-driver :phantom ~options ~bind ~@body)) (defmacro with-safari + "Executes `body` with a Safari driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `options` - optional, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-safari [driver] + (go driver \"https://clojure.org\")) + ```" [[bind & [options]] & body] `(e/with-driver :safari ~options ~bind ~@body)) (defmacro with-chrome-headless + "Executes `body` with a headless Chrome driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `options` - optional, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-chrome-headless [driver] + (go driver \"https://clojure.org\")) + ```" [[bind & [options]] & body] `(e/with-driver :chrome (assoc ~options :headless true) ~bind ~@body)) (defmacro with-firefox-headless + "Executes `body` with a headless Firefox driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `options` - optional, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-firefox-headless [driver] + (go driver \"https://clojure.org\")) + ```" [[bind & [options]] & body] `(e/with-driver :firefox (assoc ~options :headless true) ~bind ~@body)) (defmacro with-edge-headless + "Executes `body` with a headless Edge driver session bound to `bind`. + + Driver is automatically launched and terminated (even if an exception occurs). + + `options` - optional, see [Driver Options](/doc/01-user-guide.adoc#driver-options). + + Example: + + ```Clojure + (with-edge-headless [driver] + (go driver \"https://clojure.org\")) + ```" [[bind & [options]] & body] `(e/with-driver :edge (assoc ~options :headless true) ~bind ~@body)) diff --git a/src/etaoin/dev.clj b/src/etaoin/dev.clj index 0a49ef78..f53f1ec1 100644 --- a/src/etaoin/dev.clj +++ b/src/etaoin/dev.clj @@ -1,27 +1,22 @@ (ns etaoin.dev - " - A namespace to cover Chrome's devtools features. - " + "Chrome devtools features" (:require [cheshire.core :as json] [clojure.string :as str] [etaoin.api :as api])) -(defn try-parse-int +(defn- try-parse-int [line] (try (Integer/parseInt line) (catch Exception _e line))) -(defn parse-json +(defn- parse-json [string] (json/parse-string string true)) -(defn parse-method - " - Turns a string like 'Network.SomeAction' - into a keyword :network/someaction. - " +(defn- parse-method + "Turns a string like 'Network.SomeAction' into a keyword :network/someaction." [^String method] (let [[topname lowname] @@ -29,13 +24,9 @@ (str/split #"\." 2))] (keyword topname lowname))) -(defn process-log - " - Takes a log map, parses its message and merges - the message into the map. - " +(defn- process-log + "Takes a log map, parses its message and merges the message into the map." [log] - (let [{:keys [message]} log message (parse-json message) _type (some-> message :message :method parse-method)] @@ -44,28 +35,22 @@ (merge message) (assoc :_type _type)))) -(defn request? - " - True if a log entry belongs to a network domain. - " +(defn- request? + "Return true if `log` entry belongs to a network domain." [log] (some-> log :_type namespace (= "network"))) -(defn group-requests - " - Group a set of request logs by their ID. - " +(defn- group-requests + "Group a set of request `logs` by their ID." [logs] (group-by (fn [log] (some-> log :message :params :requestId)) logs)) -(defn log->request - " - A helper for a further reduce (see below). - Acc is an accumulation map. - " +(defn- log->request + "A helper for a further reduce (see below). + Acc is an accumulation map." [acc log] (let [{:keys [_type message]} log @@ -116,20 +101,13 @@ ;; default acc))) - -(defn build-request - " - Takes a vector of request logs of the same ID - and builda request map. - " +(defn- build-request + "Takes a vector of request logs of the same ID and build a request map." [logs] (reduce log->request {} logs)) - (defn logs->requests - " - From a list of log entries, create a list of requests. - " + "Return list of log entries `logs` converted to requests." [logs] (->> logs (filter request?) @@ -137,77 +115,54 @@ vals (mapv build-request))) - (defn ajax? - " - Whether it's an XHR request. - " + "Return true when `request` is XHR." [request] (:xhr? request)) - (defn logs->ajax - " - The same as `logs->requests` but return only AJAX requests. - " + "The same as [[logs->requests]] but returns only AJAX requests." [logs] (->> logs logs->requests (filterv ajax?))) - (defn request-done? - " - Whether a request has been done. It doesn't necessarily - mean it was successful though. - " + "Return true when `request` has concluded. + Completion does not indicate success. See [[request-success?]], [[request-failed?]]" [request] (:done? request)) - (defn request-failed? - " - True if a request has been failed. - " + "Return true when `request` has failed. Does not indicate completion, see [[request-done?]]" [request] (:failed? request)) - (def request-success? - "True when a request has been finished and not failed." + "Return true when `request` has completed and not failed." (every-pred request-done? (complement request-failed?))) - -;; -;; API -;; - - (defn get-performance-logs - " - Get a seq of special `performance` logs that come from - a dev console. Works only when a special `perfLoggingPrefs` - was set (see the `:dev` key when running a driver). - " + "Have `driver` return a seq of special performance logs from the dev console. + + Works only when `perfLoggingPrefs` is enabled, see [DevTools](/doc/01-user-guide.adoc#devtools)." [driver] (->> (api/get-logs driver "performance") (mapv (comp process-log api/process-log)))) - (defn get-requests - " - Return a list of HTTP requests made by a browser. - " + "Have `driver` return a list of HTTP requests made by the browser. + + Works only when `perfLoggingPrefs` is enabled, see [DevTools](/doc/01-user-guide.adoc#devtools)." [driver] (-> driver get-performance-logs logs->requests)) - (defn get-ajax - " - Return a list of XHR (Ajax) HTTP requests made by a browser. - " + "Have `driver` return a list of XHR (Ajax) HTTP requests made by the browser. + + Works only when `perfLoggingPrefs` is enabled, see [DevTools](/doc/01-user-guide.adoc#devtools)." [driver] (-> driver get-performance-logs diff --git a/src/etaoin/ide/flow.clj b/src/etaoin/ide/flow.clj index 83e3d6bb..96b7757f 100644 --- a/src/etaoin/ide/flow.clj +++ b/src/etaoin/ide/flow.clj @@ -1,7 +1,5 @@ (ns etaoin.ide.flow - " - Flow stuff (if/else, for/while/repeat, etc). - " + "Selenium IDE flow control (if/else, for/while/repeat, etc). " (:require [cheshire.core :as json] [clojure.set :as cset] @@ -12,27 +10,27 @@ (declare execute-commands) -(defn execute-branch +(defn- execute-branch [driver {:keys [this branch]} opt] (when (run-command-with-log driver this opt) (execute-commands driver branch opt) true)) -(defn execute-if +(defn- execute-if [driver {:keys [if else-if else end]} opt] (or (execute-branch driver if opt) (some #(execute-branch driver % opt) else-if) (execute-commands driver (:branch else) opt)) (run-command-with-log driver end opt)) -(defn execute-times +(defn- execute-times [driver {:keys [this branch end]} opt] (let [n (run-command-with-log driver this opt)] (doseq [commands (repeat n branch)] (execute-commands driver commands opt)) (run-command-with-log driver end opt))) -(defn execute-do +(defn- execute-do [driver {:keys [this branch repeat-if]} opt] (run-command-with-log driver this opt) (loop [commands branch] @@ -40,13 +38,13 @@ (when (run-command-with-log driver repeat-if opt) (recur commands)))) -(defn execute-while +(defn- execute-while [driver {:keys [this branch end]} opt] (while (run-command-with-log driver this opt) (execute-commands driver branch opt)) (run-command-with-log driver end opt)) -(defn execute-for-each +(defn- execute-for-each [driver {:keys [this branch end]} {vars :vars :as opt}] (let [[var-name arr] (run-command-with-log driver this opt)] (doseq [val arr] @@ -54,7 +52,7 @@ (execute-commands driver branch opt)) (run-command-with-log driver end opt))) -(defn execute-cmd-with-open-window +(defn- execute-cmd-with-open-window [driver {:keys [windowHandleName windowTimeout] :as cmd} {vars :vars :as opt}] (let [init-handles (set (e/get-window-handles driver)) _ (run-command-with-log driver cmd opt) @@ -63,7 +61,7 @@ handle (first (cset/difference final-handles init-handles))] (swap! vars assoc (str->var windowHandleName) handle))) -(defn execute-commands +(defn- execute-commands [driver commands opt] (doseq [[cmd-name cmd] commands] (case cmd-name @@ -76,7 +74,7 @@ :cmd (run-command-with-log driver cmd opt) (throw (ex-info "Command is not valid" {:command cmd}))))) -(defn run-ide-test +(defn- run-ide-test [driver {:keys [commands]} & [opt]] (let [command->kw (fn [{:keys [command] :as cmd}] (assoc cmd :command (keyword command))) @@ -87,7 +85,7 @@ {:explain-data (s/explain-data ::spec/commands commands)}))) (execute-commands driver commands-tree opt))) -(defn get-tests-by-suite-id +(defn- get-tests-by-suite-id [suite-id id {:keys [suites tests]}] (let [test-ids (-> (filter #(= suite-id (id %)) suites) first @@ -96,7 +94,7 @@ suite-tests (filter #(test-ids (:id %)) tests)] suite-tests)) -(defn find-tests +(defn ^:no-doc find-tests [{:keys [test-id test-ids suite-id suite-ids test-name suite-name test-names suite-names]} {:keys [tests] :as parsed-file}] (let [test-ids (cond-> #{} @@ -114,25 +112,20 @@ tests-found))) (defn run-ide-script - " - Run a Selenium IDE file. + "Run a Selenium IDE file. + + See [Selenium IDE docs](/doc/01-user-guide.adoc#selenium-ide) Arguments: - `driver`: a driver instance; - - `source`: either a file path, or an `io/file`, or an `io/resource`; - - `opt`: a map of optional parameters: - - -- `:test-...` and `:suite-...` (`id`, `ids`, `name`, `names`) - are used for selection of specific tests. When not passed, - all the tests get run; - - -- `:base-url` the URL of the main page from which the tests start. - Use it override the default URL from an IDE file. - " - + - `:test-...` and `:suite-...` (`id`, `ids`, `name`, `names`) + are used for selection of specific tests. When not passed, + all the tests get run; + - `:base-url` the URL of the main page from which the tests start. + Use it override the default URL from an IDE file." [driver source & [opt]] (let [parsed-file (-> source slurp diff --git a/src/etaoin/ide/main.clj b/src/etaoin/ide/main.clj index dd18af5b..7c2a0d6d 100644 --- a/src/etaoin/ide/main.clj +++ b/src/etaoin/ide/main.clj @@ -3,9 +3,11 @@ Provide an CLI entry point for running IDE files. Example: + ```shell clojure -M -m etaoin.ide.main -d firefox -p '{:port 8888 :args [\"--no-sandbox\"]}' -f /path/to/script.side + ``` - See the readme file for more info. + See the [User Guide](/doc/01-user-guide.adoc#selenium-ide-cli) for more info. " (:gen-class) (:require @@ -16,17 +18,14 @@ [etaoin.ide.flow :as flow] [etaoin.impl.util :as util])) - -(def browsers-set +(def ^:private browsers-set #{:chrome :safari :firefox :edge :phantom}) - -(defn str->vec +(defn- str->vec [string] (str/split string #",")) - -(def cli-options +(def ^:private cli-options [["-d" "--driver-name name" "The name of driver. The default is `chrome`" :default :chrome :parse-fn keyword @@ -57,8 +56,7 @@ ["-h" "--help"]]) - -(def help +(def ^:private help " This is a CLI interface for running Selenium IDE files. @@ -72,26 +70,22 @@ java -cp .../poject.jar -m etaoin.ide.main -d firefox -p '{:port 8888}' -f ide/t Options:") - -(defn usage [options-summary] +(defn ^:private usage [options-summary] (->> [help options-summary] (str/join \newline))) - -(defn error-msg [errors] +(defn ^:private error-msg [errors] (str "The following errors occurred while parsing your command:\n\n" (str/join \newline errors))) - -(def opt-fields +(def ^:private opt-fields [:base-url :test-ids :test-names :suite-ids :suite-names]) - -(defn run-script +(defn- run-script " Run a Selenium IDE file. The `source` is something that might be `slurp`ed. @@ -102,11 +96,10 @@ Options:") (api/with-driver driver-name params driver (flow/run-ide-script driver source opt)))) - (defn -main - " - The main CLI entrypoint. - " + "The main CLI entrypoint. + + See [Selenium IDE CLI docs](/doc/01-user-guide.adoc#selenium-ide-cli)" [& args] (let [{:keys [errors summary options]} (cli/parse-opts args cli-options) diff --git a/src/etaoin/keys.clj b/src/etaoin/keys.clj index 9605ab90..d3936127 100644 --- a/src/etaoin/keys.clj +++ b/src/etaoin/keys.clj @@ -1,5 +1,7 @@ (ns etaoin.keys - "https://www.w3.org/TR/webdriver/#keyboard-actions") + "Key and mouse constants. And chords. + + Sourced from [WebDriver spec](https://www.w3.org/TR/webdriver/#keyboard-actions).") (def unidentified \uE000) (def cancel \uE001) @@ -74,25 +76,35 @@ (def command meta-left) - (defn chord - [text & more] - (str (apply str text more) unidentified)) + "Apply `key` to `more` where `more` can be more keys or regular text. + Any keys that are pressed are released. -(def with-shift + Example: + ```Clojure + (chord shift-left \"help\" backspace \"lo\") + ;; would effectively be: HELLO + ```" + [key & more] + (str (apply str key more) unidentified)) + +(def ^{:arglists '([& more])} with-shift + "Apply shift key to `more` where `more` can be more keys or regular text." (partial chord shift-left)) -(def with-ctrl +(def ^{:arglists '([& more])} with-ctrl + "Apply ctrl key to `more` where `more` can be more keys or regular text." (partial chord control-left)) -(def with-alt +(def ^{:arglists '([& more])} with-alt + "Apply alt key to `more` where `more` can be more keys or regular text." (partial chord alt-left)) -(def with-command +(def ^{:arglists '([& more])} with-command + "Apply command key to `more` where `more` can be more keys or regular text." (partial chord command)) - ;; ;; Mouse codes ;; diff --git a/src/etaoin/query.clj b/src/etaoin/query.clj index 59016088..21b177fd 100644 --- a/src/etaoin/query.clj +++ b/src/etaoin/query.clj @@ -1,5 +1,9 @@ (ns etaoin.query - "A module to deal with querying elements." + "A module to deal with querying elements. + + This feels like a internal namespace. + Why do folks need to use this directly? + Maybe they are extending defmulti with more conversions?" (:require [etaoin.impl.util :as util] [etaoin.impl.xpath :as xpath])) @@ -20,6 +24,12 @@ (query locator-css term)) (defmulti to-query + "Return query for `q` for `driver`. + + Conversion depends on type of `q`. + - keyword -> converts to search on element id for q` + - string -> converts to xpath query (or css if default is changed) + - map -> converts to :xpath or :css query or map query" (fn [_driver q] (type q))) @@ -42,6 +52,8 @@ [_driver q] (util/error "Wrong query: %s" q)) -(defn expand [driver q] +(defn expand + "Return expanded query `q` for `driver`." + [driver q] (let [query (to-query driver q)] [(:locator query) (:term query)]))