#!/usr/bin/env bash # wd -- the WebDriver command line interface # # Copyright 2016 Mikael Brockman # # MIT license: # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. set -o pipefail shopt -s extglob # extended case patterns proc main { : $(WEBDRIVER_URL:=http://127.0.0.1:4444/wd/hub) setglobal cmd = $1; shift match $cmd { with new-session wd-new-session with delete-session wd-delete-session with go wd-go @Argv with back wd-post-session /back with forward wd-post-session /forward with refresh wd-post-session /refresh with get-current-url wd-get-current-url with get-title wd-get-title with get-page-source | page-source wd-get-page-source @Argv with get-window-handle wd-get-window-handle with get-window-handles wd-get-window-handles with switch-to-window wd-switch-to-window @Argv with switch-to-frame wd-switch-to-frame @Argv with switch-to-parent-frame wd-post-session /frame/parent with get-window-size wd-get-window-size with set-window-size wd-set-window-size @Argv with maximize wd-maximize with find-element | find wd-find-element @Argv with find-elements | find-all wd-find-elements @Argv with find-element-in | find-in | find-element-from-element wd-find-element-from-element @Argv with find-elements-in | find-all-in | find-elements-from-element wd-find-elements-from-element @Argv with get-element-attribute | attr | attribute wd-get-element-attribute @Argv with get-element-css-value | css-value wd-get-element-css-value @Argv with get-element-text | text wd-get-element-text @Argv with get-element-tag-name | tag-name wd-get-element-tag-name @Argv with is-element-selected | is-selected wd-is-element-selected @Argv with is-element-enabled | is-enabled wd-is-element-enabled @Argv with element-click | click wd-element-click @Argv with element-clear | clear wd-element-clear @Argv with element-send-keys | send-keys wd-element-send-keys @Argv with execute-script | execute | exec wd-execute-script-sync @Argv with execute-script-async | execute-async | exec-async wd-execute-script-async @Argv with get-all-cookies | cookies wd-get-all-cookies with get-named-cookie | cookie wd-get-named-cookie @Argv with add-cookie wd-add-cookie @Argv with delete-cookie wd-delete-cookie @Argv with delete-cookies wd-delete-cookies with take-screenshot | screenshot wd-take-screenshot with take-element-screenshot | element-screenshot wd-take-element-screenshot @Argv with help print-help with * cat > !2 << """ Usage: wd [arguments] Help: wd help """ exit 1 } } proc die { setglobal rc = $1; shift; echo "$0:" @Argv > !2; exit "$rc" } proc json-obj { perl -mJSON::PP -e ' use List::Util qw(pairmap); $j = JSON::PP->new->allow_nonref; print "{", join(",", pairmap { $j->encode($a) .":". $b } @ARGV), "}" ' @Argv } proc json-arr { perl -e ' print "[", join(",", @ARGV), "]" ' @Argv } proc json-str { perl -mJSON::PP -e 'print JSON::PP->new->allow_nonref->encode($ARGV[0])' $1 } proc json-get { perl -mJSON::PP -e ' local $/; my $s = || exit 1; my $j = JSON::PP->new->allow_nonref; my $data = $j->decode($s); my $x = $data; foreach (@ARGV) { if (ref($x) eq "ARRAY") { # convert array to k=>v hash my %h = map { $_ => $x->[$_] } 0..$#$x; $x = \%h; } exists $x->{$_} or die "invalid JSON index"; $x = $x->{$_}; } print $j->encode($x), "\n"; ' @Argv } proc json-raw { perl -mJSON::PP -e ' local $/; my $s = || exit 1; my $j = JSON::PP->new->allow_nonref; sub flat { my $x = shift; if (ref($x) eq "ARRAY") { flat($_) foreach @$x; } elsif (ref($x) eq "HASH") { flat($_) foreach values %$x; } else { print $x, "\n"; } } flat $j->decode($s); ' } proc json-raw-nonnull { local x setglobal x = $[cat] match $x { with null false with * json-raw <<<$x } } proc json-get-raw { json-get @Argv | json-raw; } proc json-get-raw-nonnull { json-get @Argv | json-raw-nonnull; } proc decode-base64 { perl -MMIME::Base64 -e 'print decode_base64()' } proc hush { @Argv >/dev/null; } proc wd-post { if [[ $# -eq 2 ]] { curl -sSL -H "Content-Type: application/json" --data-raw $2 \ "$WEBDRIVER_URL""$1" } elif [[ $# -eq 1 ]] { curl -sSL -XPOST "$WEBDRIVER_URL""$1" } else { die 127 internal error } } proc wd-new-session { local capabilities if [[ -n "$WEBDRIVER_CAPABILITIES" ]] { setglobal capabilities = $WEBDRIVER_CAPABILITIES } elif [[ -n "$WEBDRIVER_BROWSER" ]] { setglobal capabilities = $[json-obj browserName $[json-str $WEBDRIVER_BROWSER]] } else { die 1 "neither WEBDRIVER_CAPABILITIES nor WEBDRIVER_BROWSER set" } wd-post /session $[json-obj desiredCapabilities $capabilities] | json-get-raw-nonnull sessionId } proc wd-assert-session { if [[ -z "$WEBDRIVER_SESSION" ]] { die 1 WEBDRIVER_SESSION not defined } } proc wd-get-session { wd-assert-session curl -sSL "$WEBDRIVER_URL"/session/"$WEBDRIVER_SESSION""$1" } proc wd-checked-result { local result setglobal result = $[cat] setglobal status = $[json-get status <<<$result] if [[ $status -ne 0 ]] { die 1 $[json-get-raw value message <<<$result] } else { json-get value <<<$result } } proc wd-post-session-value { wd-post-session @Argv | wd-checked-result; } proc wd-get-session-simple { wd-get-session @Argv | json-get-raw-nonnull value; } proc wd-get-session-value { wd-get-session @Argv | wd-checked-result; } proc wd-get-session-value-raw { wd-get-session-value @Argv | json-raw-nonnull; } proc wd-get-element-value { wd-get-session-value-raw /element/"$1"/"$2"; } proc wd-get-current-url { wd-get-session-simple /url; } proc wd-get-title { wd-get-session-simple /title; } proc wd-get-window-handle { wd-get-session-simple /window; } proc wd-get-page-source { wd-get-session-value /source | json-raw; } proc wd-delete-session { wd-assert-session curl -sSL -XDELETE "$WEBDRIVER_URL"/session/"$WEBDRIVER_SESSION""$1" } proc wd-post-session { wd-assert-session setglobal url = $1; shift wd-post /session/"$WEBDRIVER_SESSION""$url" @Argv | wd-checked-result } proc wd-go { hush wd-post-session /url $[json-obj url $[json-str $1]] } proc wd-close-window { hush wd-delete-session /window } proc wd-switch-to-window { hush wd-post-session /window $[json-obj handle $[json-str $1]] } proc wd-get-window-handles { wd-get-session /window/handles | json-get-raw value } proc wd-switch-to-frame { local id match $1 { with [0-9] setglobal id = $1 with * setglobal id = $[json-str $1] } hush wd-post-session /frame $[json-obj id $id] } proc wd-switch-to-top-level-frame { hush wd-post-session frame $[json-obj id null] } proc wd-get-window-size { local result setglobal result = $[wd-get-session /window/size] echo $result | json-get-raw value width echo $result | json-get-raw value height } proc wd-set-window-size { hush wd-post-session /window/size $[json-obj width $1 height $2] } proc wd-maximize { hush wd-post-session /window/maximize } proc wd-parse-strategy { match $1 { with id json-str "id" with name json-str "name" with class* json-str "class name" with tag* json-str "tag name" with css* json-str "css selector" with link* json-str "link text" with partial* json-str "partial link text" with xpath* json-str xpath with * die 1 unknown element location strategy: $1 } } proc wd-general-element-find { local path="$1"; shift local strategy setglobal strategy = $[wd-parse-strategy $1] [[ $? -eq 0 ]] || exit $? shift local value setglobal value = $[json-str "$ifsjoin(Argv)] wd-post-session $path $[json-obj using $strategy value $value] | json-raw } proc wd-find-element { wd-general-element-find /element @Argv; } proc wd-find-elements { wd-general-element-find /elements @Argv; } proc wd-find-element-from-element { local element=$1; shift wd-general-element-find /element/"$element"/element @Argv } proc wd-find-elements-from-element { local element=$1; shift wd-general-element-find /element/"$element"/elements @Argv } proc wd-get-element-attribute { local x setglobal x = $[wd-get-session-value-raw /element/"$1"/attribute/"$2] [[ $? -eq 0 ]] || die 1 "attribute not found" echo $x } proc wd-get-element-css-value { local x setglobal x = $[wd-get-session-value-raw /element/"$1"/css/"$2] [[ $? -eq 0 ]] || die 1 "CSS property not found" echo $x } proc wd-get-element-text { wd-get-element-value $1 text; } proc wd-get-element-tag-name { wd-get-element-value $1 name; } proc wd-is-element-enabled { [[ 1 -eq $(wd-get-element-value "$1" enabled) ]]; } proc wd-is-element-selected { [[ 1 -eq $(wd-get-element-value "$1" selected) ]]; } proc wd-element-simple-action { hush wd-post-session /element/"$1"/"$2" } proc wd-element-click { wd-element-simple-action $1 click; } proc wd-element-clear { wd-element-simple-action $1 clear; } proc wd-element-send-keys { local element setglobal element = $1; shift wd-element-simple-action $element value $[json-obj value $[json-str "$ifsjoin(Argv)]] } proc wd-execute-script-sync { local script="$1"; shift wd-post-session /execute/sync $[json-obj \ script $[json-str $script] \ args $[json-arr @Argv]] } proc wd-execute-script-async { local script="$1"; shift wd-post-session /execute/async $[json-obj \ script $[json-str $script] \ args $[json-arr @Argv]] } proc wd-get-all-cookies { wd-get-session-value /cookie; } proc wd-get-named-cookie { wd-get-session-value /cookie/"$1"; } proc wd-add-cookie { hush wd-post-session /cookie $[json-obj cookie $[json-obj @Argv]] } proc wd-delete-cookie { wd-delete-session /cookie/"$1"; } proc wd-delete-cookies { wd-delete-session /cookie; } proc wd-take-screenshot { wd-get-session-value /screenshot | json-raw | decode-base64 } proc wd-take-element-screenshot { wd-get-session-value /element/"$1"/screenshot | json-raw | decode-base64 } proc print-help { cat << ''' # wd -- WebDriver command line tool `wd` is a simple tool for interacting with servers that implement the W3C WebDriver API. It can be used for web automation tasks such as testing and scraping. You can use [Selenium](http://www.seleniumhq.org/) as the WebDriver server to control browsers on your own machine. There are commercial services that offer the WebDriver API remotely; see "Functional Test Services" [here](http://www.seleniumhq.org/ecosystem/). See [the WebDriver spec](https://w3c.github.io/webdriver/webdriver-spec.html) for details about the protocol and behavior. ## Dependencies - `bash` - `perl` (5.14 or greater) - `curl` ## Example session $ export WEBDRIVER_BROWSER=chrome $ export WEBDRIVER_SESSION="$(wd new-session)" $ wd go https://github.com/mbrock/wd $ wd screenshot > /tmp/wd.png $ wd click "$(wd find css .user-mention)" $ wd exec 'return document.title' ## Configuration - `WEBDRIVER_URL`: WebDriver API URL (default `http://127.0.0.1:4444/wd/hub`) ## Command reference ### Managing sessions #### `wd new-session` Prints the new session ID. All other commands expect this ID to be in `WEBDRIVER_SESSION`, so export WEBDRIVER_SESSION="$(wd new-session)" is a useful pattern. You must configure desired capabilities by setting either - `WEBDRIVER_CAPABILITIES` to a stringified JSON object, or - `WEBDRIVER_BROWSER` to a browser name (`chrome`, `firefox`, etc). #### `wd delete-session` Deletes the current session. ### Navigation #### `wd go ` Opens `` in the current window. #### `wd back` Navigates back in the current window. #### `wd forward` Navigates forward in the current window. #### `wd refresh` Refreshes the page of the current window. ### Element finding #### `wd find ...` Finds one matching element and prints its element ID. The `` can be one of: - `css` (CSS selector) - `xpath` (XPath selector) - `id` (element ID) - `name` (element name) - `class` (element class name) - `tag` (element tag name) - `link` (element link text) - `partial` (partial element link text) The `` values are concatenated for convenience. Example: $ wd find css article header img.avatar #### `wd find-all ...` See `wd find`; finds all matching elements. #### `wd find-in ...` See `wd find`; finds one matching sub-element. #### `wd find-all-in ...` See `wd find`; finds all matching sub-elements. ### Element information #### `wd is-selected ` Exits with a non-zero status if the element is not a selected or checked `input` or `option`. #### `wd is-enabled ` Exits with a non-zero status if the element is not an enabled form control. #### `wd attribute ` Prints an element attribute value. Exits with non-zero status if the given attribute does not exist. #### `wd css-value ` Prints an element CSS property value. Exits with non-zero status if the given style property does not exist. #### `wd text ` Prints an element's `innerText`. #### `wd tag-name ` Prints the tag name of an element. ### Element actions #### `wd click ` Clicks an element. #### `wd clear ` Clears the value, checkedness, or text content of an element. #### `wd send-keys [keys] ...` Sends keys to an element. Key arguments are concatenated for convenience. Example: $ wd send-keys "$(wd find id search)" webdriver json api ### JavaScript execution #### `wd execute [argument] ...` Evaluates the JavaScript code `` as a function called with the given arguments. Prints the return value of the specified function. #### `wd execute-async [argument] ...` Evaluates as in `wd execute` but waiting for the script to invoke a callback which is passed as an additional final argument to the specified function. Prints the value finally passed to the callback. ### Page information #### `wd get-current-url` Prints the URL of the page in the current window. #### `wd get-title` Prints the title of the page in the current window. #### `wd get-page-source` Prints the raw HTML source of the page in the current window. ### Windows #### `wd get-window-size` Prints the current window's width and height on separate lines. #### `wd set-window-size ` Changes the size of the current window. #### `wd maximize` Maximizes the current window. #### `wd get-window-handle` Prints the window handle of the current window. #### `wd get-window-handles` Prints a list of all window handles in the current session. #### `wd switch-to-window ` Changes which window is the current window. ### Frames #### `wd switch-to-frame ` Changes the current frame. `` can be either a number or an element ID. See [the specification](https://www.w3.org/TR/webdriver/#switch-to-frame) for exact details. #### `wd switch-to-top-level-frame` Resets the current frame to the top level. #### `wd switch-to-parent-frame` Sets the current frame to the parent of the current frame. ### Cookies See [the spec](https://w3c.github.io/webdriver/webdriver-spec.html#cookies) for details on cookie JSON serialization. #### `wd cookies` Prints the currently set cookies as a JSON array. #### `wd cookie ` Prints the cookie named `` as JSON. #### `wd add-cookie ...` Adds a cookie according to the given keys/values. Example: `wd add-cookie name '"user"' value '"mbrock"'` #### `wd delete-cookie ` Deletes the cookie whose name is ``. #### `wd delete-cookies` Deletes all cookies. ### Screenshots #### `wd screenshot` Prints a binary PNG screenshot to stdout. #### `wd element-screenshot ` Prints a binary PNG screenshot of a specific element to stdout. (Not supported by Chrome.) ''' } main @Argv