blob: 724332d959d10ee7496c866a891ea2db91301d4f [file] [log] [blame]
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library webdriver;
import 'dart:async';
import 'dart:io';
import 'dart:json' as json;
import 'dart:uri';
part 'src/base64decoder.dart';
/**
* WebDriver bindings for Dart.
*
* ## Installing ##
*
* Use [pub][] to install this package. Add the following to your `pubspec.yaml`
* file.
*
* dependencies:
* webdriver: any
*
* Then run `pub install`.
*
* For more information, see the
* [webdriver package on pub.dartlang.org][pkg].
*
* ## Using ##
*
* These bindings are based on the WebDriver JSON wire protocol spec
* (http://code.google.com/p/selenium/wiki/JsonWireProtocol). Not
* all of these commands are implemented yet by WebDriver itself.
* Nontheless this is a complete implementation of the spec as the
* unsupported commands may be supported in the future. Currently,
* there are known issues with local and session storage, script
* execution, and log access.
*
* To use these bindings, the Selenium standalone server must be running.
* You can download it at http://code.google.com/p/selenium/downloads/list.
*
* There are a number of commands that use ids to access page elements.
* These ids are not the HTML ids; they are opaque ids internal to
* WebDriver. To get the id for an element you would first need to do
* a search, get the results, and extract the WebDriver id from the returned
* [Map] using the 'ELEMENT' key. For example:
*
* String id;
* WebDriverSession session;
* Future f = web_driver.newSession('chrome');
* f.then((_session) {
* session = _session;
* return session.setUrl('http://my.web.site.com');
* }).then((_) {
* return session.findElement('id', 'username');
* }).then((element) {
* id = element['ELEMENT'];
* return session.sendKeyStrokesToElement(id,
* [ 'j', 'o', 'e', ' ', 'u', 's', 'e', 'r' ]);
* }).then((_) {
* return session.submit(id);
* }).then((_) {
* return session.close();
* }).then((_) {
* session = null;
* });
*
* [pub]: http://pub.dartlang.org
* [pkg]: http://pub.dartlang.org/packages/webdriver
*/
void writeStringToFile(String fileName, String contents) {
new File(fileName).writeAsStringSync(contents);
}
void writeBytesToFile(String fileName, List<int> contents) {
new File(fileName).writeAsBytesSync(contents);
}
class WebDriverError {
static List _errorTypes = null;
static List _errorDetails = null;
int statusCode;
String type;
String message;
String details;
String results;
WebDriverError(this.statusCode, this.message, [this.results = '']) {
/** These correspond to WebDrive exception types. */
if (_errorTypes == null) {
_errorTypes = [
null,
'IndexOutOfBounds',
'NoCollection',
'NoString',
'NoStringLength',
'NoStringWrapper',
'NoSuchDriver',
'NoSuchElement',
'NoSuchFrame',
'UnknownCommand',
'ObsoleteElement',
'ElementNotDisplayed',
'InvalidElementState',
'Unhandled',
'Expected',
'ElementNotSelectable',
'NoSuchDocument',
'UnexpectedJavascript',
'NoScriptResult',
'XPathLookup',
'NoSuchCollection',
'TimeOut',
'NullPointer',
'NoSuchWindow',
'InvalidCookieDomain',
'UnableToSetCookie',
'UnexpectedAlertOpen',
'NoAlertOpen',
'ScriptTimeout',
'InvalidElementCoordinates',
'IMENotAvailable',
'IMEEngineActivationFailed',
'InvalidSelector',
'SessionNotCreatedException',
'MoveTargetOutOfBounds'
];
// Explanations of the eror types. In thoses cases where the
// explanation is the same as the type (e.g. NoCollection), that is an
// error type used by an old version of the IE driver and is deprecated.
_errorDetails = [
null,
'IndexOutOfBounds',
'NoCollection',
'NoString',
'NoStringLength',
'NoStringWrapper',
'NoSuchDriver',
'An element could not be located on the page using the given '
'search parameters.',
'A request to switch to a frame could not be satisfied because the '
'frame could not be found.',
'The requested resource could not be found, or a request was '
'received using an HTTP method that is not supported by the '
'mapped resource.',
'An element command failed because the referenced element is no '
'longer attached to the DOM.',
'An element command could not be completed because the element '
'is not visible on the page.',
'An element command could not be completed because the element is in '
'an invalid state (e.g. attempting to click a disabled element).',
'An unknown server-side error occurred while processing the command.',
'Expected',
'An attempt was made to select an element that cannot be selected.',
'NoSuchDocument',
'An error occurred while executing user supplied JavaScript.',
'NoScriptResult',
'An error occurred while searching for an element by XPath.',
'NoSuchCollection',
'An operation did not complete before its timeout expired.',
'NullPointer',
'A request to switch to a different window could not be satisfied '
'because the window could not be found.',
'An illegal attempt was made to set a cookie under a different '
'domain than the current page.',
'A request to set a cookie\'s value could not be satisfied.',
'A modal dialog was open, blocking this operation.',
'An attempt was made to operate on a modal dialog when one was '
'not open.',
'A script did not complete before its timeout expired.',
'The coordinates provided to an interactions operation are invalid.',
'IME was not available.',
'An IME engine could not be started.',
'Argument was an invalid selector (e.g. XPath/CSS).',
'A new session could not be created.',
'Target provided for a move action is out of bounds.'
];
}
if (statusCode < 0 || statusCode > 32) {
type = 'External';
details = '';
} else {
type = _errorTypes[statusCode];
details = _errorDetails[statusCode];
}
}
String toString() {
return '$statusCode $type: $message $results\n$details';
}
}
/**
* Base class for all WebDriver request classes. This class wraps up
* an URL prefix (host, port, and initial path), and provides a client
* function for doing HTTP requests with JSON payloads.
*/
class WebDriverBase {
String _host;
int _port;
String _path;
String _url;
String get path => _path;
String get url => _url;
/**
* The default URL for WebDriver remote server is
* http://localhost:4444/wd/hub.
*/
WebDriverBase.fromUrl([this._url = 'http://localhost:4444/wd/hub']) {
// Break out the URL components.
var re = new RegExp('[^:/]+://([^/]+)(/.*)');
var matches = re.firstMatch(_url);
_host = matches[1];
_path = matches[2];
var idx = _host.indexOf(':');
if (idx >= 0) {
_port = int.parse(_host.substring(idx+1));
_host = _host.substring(0, idx);
} else {
_port = 80;
}
}
WebDriverBase([
this._host = 'localhost',
this._port = 4444,
this._path = '/wd/hub']) {
_url = 'http://$_host:$_port$_path';
}
void _failRequest(Completer completer, error, [stackTrace]) {
if (completer != null) {
var trace = stackTrace != null ? stackTrace : getAttachedStackTrace(error);
completer.completeError(new WebDriverError(-1, error), trace);
}
}
/**
* Execute a request to the WebDriver server. [http_method] should be
* one of 'GET', 'POST', or 'DELETE'. [command] is the text to append
* to the base URL path to get the full URL. [params] are the additional
* parameters. If a [List] or [Map] they will be posted as JSON parameters.
* If a number or string, "/params" is appended to the URL.
*/
void _serverRequest(String http_method, String command, Completer completer,
{List successCodes, params, Function customHandler}) {
var status = 0;
var results = null;
var message = null;
if (successCodes == null) {
successCodes = [ 200, 204 ];
}
try {
var path = command;
if (params != null) {
if (params is num || params is String) {
path = '$path/$params';
params = null;
} else if (http_method != 'POST') {
throw new Exception(
'The http method called for ${command} is ${http_method} but it '
'must be POST if you want to pass the JSON params '
'${json.stringify(params)}');
}
}
var client = new HttpClient();
client.open(http_method, _host, _port, path).then((req) {
req.followRedirects = false;
req.headers.add(HttpHeaders.ACCEPT, "application/json");
req.headers.add(
HttpHeaders.CONTENT_TYPE, 'application/json;charset=UTF-8');
if (params != null) {
var body = json.stringify(params);
req.write(body);
}
req.close().then((rsp) {
List<int> body = new List<int>();
rsp.listen(body.addAll, onDone: () {
var value = null;
// For some reason we get a bunch of NULs on the end
// of the text and the json.parse blows up on these, so
// strip them with trim().
// These NULs can be seen in the TCP packet, so it is not
// an issue with character encoding; it seems to be a bug
// in WebDriver stack.
results = new String.fromCharCodes(body).trim();
if (!successCodes.contains(rsp.statusCode)) {
_failRequest(completer,
'Unexpected response ${rsp.statusCode}; $results');
completer = null;
return;
}
if (status == 0 && results.length > 0) {
// 4xx responses send plain text; others send JSON.
if (rsp.statusCode < 400) {
results = json.parse(results);
status = results['status'];
}
if (results is Map && (results as Map).containsKey('value')) {
value = results['value'];
}
if (value is Map && value.containsKey('message')) {
message = value['message'];
}
}
if (status == 0) {
if (customHandler != null) {
customHandler(rsp, value);
} else if (completer != null) {
completer.complete(value);
}
}
}, onError: (error) {
_failRequest(completer, error);
completer = null;
});
})
.catchError((error) {
_failRequest(completer, error);
completer = null;
});
})
.catchError((error) {
_failRequest(completer, error);
completer = null;
});
} catch (e, s) {
_failRequest(completer, e, s);
completer = null;
}
}
Future _get(String extraPath,
[Completer completer, Function customHandler]) {
if (completer == null) completer = new Completer();
_serverRequest('GET', '${_path}/$extraPath', completer,
customHandler: customHandler);
return completer.future;
}
Future _post(String extraPath, [params]) {
var completer = new Completer();
_serverRequest('POST', '${_path}/$extraPath', completer,
params: params);
return completer.future;
}
Future _delete(String extraPath) {
var completer = new Completer();
_serverRequest('DELETE', '${_path}/$extraPath', completer);
return completer.future;
}
}
class WebDriver extends WebDriverBase {
WebDriver(host, port, path) : super(host, port, path);
/**
* Create a new session. The server will attempt to create a session that
* most closely matches the desired and required capabilities. Required
* capabilities have higher priority than desired capabilities and must be
* set for the session to be created.
*
* The capabilities are:
*
* - browserName (String) The name of the browser being used; should be one
* of chrome|firefox|htmlunit|internet explorer|iphone.
*
* - version (String) The browser version, or the empty string if unknown.
*
* - platform (String) A key specifying which platform the browser is
* running on. This value should be one of WINDOWS|XP|VISTA|MAC|LINUX|UNIX.
* When requesting a new session, the client may specify ANY to indicate
* any available platform may be used.
*
* - javascriptEnabled (bool) Whether the session supports executing user
* supplied JavaScript in the context of the current page.
*
* - takesScreenshot (bool) Whether the session supports taking screenshots
* of the current page.
*
* - handlesAlerts (bool) Whether the session can interact with modal popups,
* such as window.alert and window.confirm.
*
* - databaseEnabled (bool) Whether the session can interact database storage.
*
* - locationContextEnabled (bool) Whether the session can set and query the
* browser's location context.
*
* - applicationCacheEnabled (bool) Whether the session can interact with
* the application cache.
*
* - browserConnectionEnabled (bool) Whether the session can query for the
* browser's connectivity and disable it if desired.
*
* - cssSelectorsEnabled (bool) Whether the session supports CSS selectors
* when searching for elements.
*
* - webStorageEnabled (bool) Whether the session supports interactions with
* storage objects.
*
* - rotatable (bool) Whether the session can rotate the current page's
* current layout between portrait and landscape orientations (only applies
* to mobile platforms).
*
* - acceptSslCerts (bool) Whether the session should accept all SSL certs
* by default.
*
* - nativeEvents (bool) Whether the session is capable of generating native
* events when simulating user input.
*
* - proxy (proxy object) Details of any proxy to use. If no proxy is
* specified, whatever the system's current or default state is used.
*
* The format of the proxy object is:
*
* - proxyType (String) The type of proxy being used. Possible values are:
*
* direct - A direct connection - no proxy in use,
*
* manual - Manual proxy settings configured,
*
* pac - Proxy autoconfiguration from a URL),
*
* autodetect (proxy autodetection, probably with WPAD),
*
* system - Use system settings
*
* - proxyAutoconfigUrl (String) Required if proxyType == pac, Ignored
* otherwise. Specifies the URL to be used for proxy autoconfiguration.
*
* - ftpProxy, httpProxy, sslProxy (String) (Optional, Ignored if
* proxyType != manual) Specifies the proxies to be used for FTP, HTTP
* and HTTPS requests respectively. Behaviour is undefined if a request
* is made, where the proxy for the particular protocol is undefined,
* if proxyType is manual.
*
* Potential Errors: SessionNotCreatedException (if a required capability
* could not be set).
*/
Future<WebDriverSession> newSession([
browser = 'chrome', Map additional_capabilities]) {
var completer = new Completer();
if (additional_capabilities == null) {
additional_capabilities = {};
}
additional_capabilities['browserName'] = browser;
_serverRequest('POST', '${_path}/session', completer,
successCodes: [ 302 ],
customHandler: (r, v) {
var url = r.headers.value(HttpHeaders.LOCATION);
var session = new WebDriverSession.fromUrl(url);
completer.complete(session);
},
params: { 'desiredCapabilities': additional_capabilities });
return completer.future;
}
/** Get the set of currently active sessions. */
Future<List<WebDriverSession>> getSessions() {
var completer = new Completer();
return _get('sessions', completer, (r, v) {
var _sessions = [];
for (var session in v) {
var url = 'http://${this._host}:${this._port}${this._path}/'
'session/${session["id"]}';
_sessions.add(new WebDriverSession.fromUrl(url));
}
completer.complete(_sessions);
});
}
/** Query the server's current status. */
Future<Map> getStatus() => _get('status');
}
class WebDriverWindow extends WebDriverBase {
WebDriverWindow.fromUrl(url) : super.fromUrl(url);
/** Get the window size. */
Future<Map> getSize() => _get('size');
/**
* Set the window size. Note that this is flaky and often
* has no effect.
*
* Potential Errors:
* NoSuchWindow - If the specified window cannot be found.
*/
Future<String> setSize(int width, int height) =>
_post('size', { 'width': width, 'height': height });
/** Get the window position. */
Future<Map> getPosition() => _get('position');
/**
* Set the window position. Note that this is flaky and often
* has no effect.
*
* Potential Errors: NoSuchWindow.
*/
Future setPosition(int x, int y) =>
_post('position', { 'x': x, 'y': y });
/** Maximize the specified window if not already maximized. */
Future maximize() => _post('maximize');
}
class WebDriverSession extends WebDriverBase {
WebDriverSession.fromUrl(url) : super.fromUrl(url);
/** Close the session. */
Future close() => _delete('');
/** Get the session capabilities. See [newSession] for details. */
Future<Map> getCapabilities() => _get('');
/**
* Configure the amount of time in milliseconds that a script can execute
* for before it is aborted and a Timeout error is returned to the client.
*/
Future setScriptTimeout(t) =>
_post('timeouts', { 'type': 'script', 'ms': t });
/*Future<String> setImplicitWaitTimeout(t) =>
simplePost('timeouts', { 'type': 'implicit', 'ms': t });*/
/**
* Configure the amount of time in milliseconds that a page can load for
* before it is aborted and a Timeout error is returned to the client.
*/
Future setPageLoadTimeout(t) =>
_post('timeouts', { 'type': 'page load', 'ms': t });
/**
* Set the amount of time, in milliseconds, that asynchronous scripts
* executed by /session/:sessionId/execute_async are permitted to run
* before they are aborted and a Timeout error is returned to the client.
*/
Future setAsyncScriptTimeout(t) =>
_post('timeouts/async_script', { 'ms': t });
/**
* Set the amount of time the driver should wait when searching for elements.
* When searching for a single element, the driver should poll the page until
* an element is found or the timeout expires, whichever occurs first. When
* searching for multiple elements, the driver should poll the page until at
* least one element is found or the timeout expires, at which point it should
* return an empty list.
*
* If this command is never sent, the driver should default to an implicit
* wait of 0ms.
*/
Future setImplicitWaitTimeout(t) =>
_post('timeouts/implicit_wait', { 'ms': t });
/**
* Retrieve the current window handle.
*
* Potential Errors: NoSuchWindow.
*/
Future<String> getWindowHandle() => _get('window_handle');
/**
* Retrieve a [WebDriverWindow] for the specified window. We don't
* have to use a Future here but do so to be consistent.
*/
Future<WebDriverWindow> getWindow([handle = 'current']) {
var completer = new Completer();
completer.complete(new WebDriverWindow.fromUrl('${_url}/window/$handle'));
return completer.future;
}
/** Retrieve the list of all window handles available to the session. */
Future<List<String>> getWindowHandles() => _get('window_handles');
/**
* Retrieve the URL of the current page.
*
* Potential Errors: NoSuchWindow.
*/
Future<String> getUrl() => _get('url');
/**
* Navigate to a new URL.
*
* Potential Errors: NoSuchWindow.
*/
Future setUrl(String url) => _post('url', { 'url': url });
/**
* Navigate forwards in the browser history, if possible.
*
* Potential Errors: NoSuchWindow.
*/
Future navigateForward() => _post('forward');
/**
* Navigate backwards in the browser history, if possible.
*
* Potential Errors: NoSuchWindow.
*/
Future navigateBack() => _post('back');
/**
* Refresh the current page.
*
* Potential Errors: NoSuchWindow.
*/
Future refresh() => _post('refresh');
/**
* Inject a snippet of JavaScript into the page for execution in the context
* of the currently selected frame. The executed script is assumed to be
* synchronous and the result of evaluating the script is returned to the
* client.
*
* The script argument defines the script to execute in the form of a
* function body. The value returned by that function will be returned to
* the client. The function will be invoked with the provided args array
* and the values may be accessed via the arguments object in the order
* specified.
*
* Arguments may be any JSON-primitive, array, or JSON object. JSON objects
* that define a WebElement reference will be converted to the corresponding
* DOM element. Likewise, any WebElements in the script result will be
* returned to the client as WebElement JSON objects.
*
* Potential Errors: NoSuchWindow, StaleElementReference, JavaScriptError.
*/
Future execute(String script, [List args]) {
if (args == null) args = [];
return _post('execute', { 'script': script, 'args': args });
}
/**
* Inject a snippet of JavaScript into the page for execution in the context
* of the currently selected frame. The executed script is assumed to be
* asynchronous and must signal that it is done by invoking the provided
* callback, which is always provided as the final argument to the function.
* The value to this callback will be returned to the client.
*
* Asynchronous script commands may not span page loads. If an unload event
* is fired while waiting for a script result, an error should be returned
* to the client.
*
* The script argument defines the script to execute in the form of a function
* body. The function will be invoked with the provided args array and the
* values may be accessed via the arguments object in the order specified.
* The final argument will always be a callback function that must be invoked
* to signal that the script has finished.
*
* Arguments may be any JSON-primitive, array, or JSON object. JSON objects
* that define a WebElement reference will be converted to the corresponding
* DOM element. Likewise, any WebElements in the script result will be
* returned to the client as WebElement JSON objects.
*
* Potential Errors: NoSuchWindow, StaleElementReference, Timeout (controlled
* by the [setAsyncScriptTimeout] command), JavaScriptError (if the script
* callback is not invoked before the timout expires).
*/
Future executeAsync(String script, [List args]) {
if (args == null) args = [];
return _post('execute_async', { 'script': script, 'args': args });
}
/**
* Take a screenshot of the current page (PNG).
*
* Potential Errors: NoSuchWindow.
*/
Future<List<int>> getScreenshot([fname]) {
var completer = new Completer();
return _get('screenshot', completer, (r, v) {
var image = Base64Decoder.decode(v);
if (fname != null) {
writeBytesToFile(fname, image);
}
completer.complete(image);
});
}
/**
* List all available IME (Input Method Editor) engines on the machine.
* To use an engine, it has to be present in this list.
*
* Potential Errors: ImeNotAvailableException.
*/
Future<List<String>> getAvailableImeEngines() =>
_get('ime/available_engines');
/**
* Get the name of the active IME engine. The name string is
* platform specific.
*
* Potential Errors: ImeNotAvailableException.
*/
Future<String> getActiveImeEngine() => _get('ime/active_engine');
/**
* Indicates whether IME input is active at the moment (not if
* it's available).
*
* Potential Errors: ImeNotAvailableException.
*/
Future<bool> getIsImeActive() => _get('ime/activated');
/**
* De-activates the currently-active IME engine.
*
* Potential Errors: ImeNotAvailableException.
*/
Future deactivateIme() => _post('ime/deactivate');
/**
* Make an engine that is available (appears on the list returned by
* getAvailableEngines) active. After this call, the engine will be added
* to the list of engines loaded in the IME daemon and the input sent using
* sendKeys will be converted by the active engine. Note that this is a
* platform-independent method of activating IME (the platform-specific way
* being using keyboard shortcuts).
*
* Potential Errors: ImeActivationFailedException, ImeNotAvailableException.
*/
Future activateIme(String engine) =>
_post('ime/activate', { 'engine': engine });
/**
* Change focus to another frame on the page. If the frame id is null,
* the server should switch to the page's default content.
* [id] is the Identifier for the frame to change focus to, and can be
* a string, number, null, or JSON Object.
*
* Potential Errors: NoSuchWindow, NoSuchFrame.
*/
Future setFrameFocus(id) => _post('frame', { 'id': id });
/**
* Change focus to another window. The window to change focus to may be
* specified by [name], which is its server assigned window handle, or
* the value of its name attribute.
*
* Potential Errors: NoSuchWindow.
*/
Future setWindowFocus(name) => _post('window', { 'name': name });
/**
* Close the current window.
*
* Potential Errors: NoSuchWindow.
*/
Future closeWindow() => _delete('window');
/**
* Retrieve all cookies visible to the current page.
*
* The returned List contains Maps with the following keys:
*
* 'name' - The name of the cookie.
*
* 'value' - The cookie value.
*
* The following keys may optionally be present:
*
* 'path' - The cookie path.
*
* 'domain' - The domain the cookie is visible to.
*
* 'secure' - Whether the cookie is a secure cookie.
*
* 'expiry' - When the cookie expires, seconds since midnight, 1/1/1970 UTC.
*
* Potential Errors: NoSuchWindow.
*/
Future<List<Map>> getCookies() => _get('cookie');
/**
* Set a cookie. If the cookie path is not specified, it should be set
* to "/". Likewise, if the domain is omitted, it should default to the
* current page's domain. See [getCookies] for the structure of a cookie
* Map.
*/
Future setCookie(Map cookie) => _post('cookie', { 'cookie': cookie });
/**
* Delete all cookies visible to the current page.
*
* Potential Errors: InvalidCookieDomain (the cookie's domain is not
* visible from the current page), NoSuchWindow, UnableToSetCookie (if
* attempting to set a cookie on a page that does not support cookies,
* e.g. pages with mime-type text/plain).
*/
Future deleteCookies() => _delete('cookie');
/**
* Delete the cookie with the given [name]. This command should be a no-op
* if there is no such cookie visible to the current page.
*
* Potential Errors: NoSuchWindow.
*/
Future deleteCookie(String name) => _delete('cookie/$name');
/**
* Get the current page source.
*
* Potential Errors: NoSuchWindow.
*/
Future<String> getPageSource() => _get('source');
/**
* Get the current page title.
*
* Potential Errors: NoSuchWindow.
*/
Future<String> getPageTitle() => _get('title');
/**
* Search for an element on the page, starting from the document root. The
* first matching located element will be returned as a WebElement JSON
* object (a [Map] with an 'ELEMENT' key whose value should be used to
* identify the element in further requests). The [strategy] should be
* one of:
*
* 'class name' - Returns an element whose class name contains the search
* value; compound class names are not permitted.
*
* 'css selector' - Returns an element matching a CSS selector.
*
* 'id' - Returns an element whose ID attribute matches the search value.
*
* 'name' - Returns an element whose NAME attribute matches the search value.
*
* 'link text' - Returns an anchor element whose visible text matches the
* search value.
*
* 'partial link text' - Returns an anchor element whose visible text
* partially matches the search value.
*
* 'tag name' - Returns an element whose tag name matches the search value.
*
* 'xpath' - Returns an element matching an XPath expression.
*
* Potential Errors: NoSuchWindow, NoSuchElement, XPathLookupError (if
* using XPath and the input expression is invalid).
*/
Future<String> findElement(String strategy, String searchValue) =>
_post('element', { 'using': strategy, 'value' : searchValue });
/**
* Search for multiple elements on the page, starting from the document root.
* The located elements will be returned as WebElement JSON objects. See
* [findElement] for the locator strategies that each server supports.
* Elements are be returned in the order located in the DOM.
*
* Potential Errors: NoSuchWindow, XPathLookupError.
*/
Future<List<String>> findElements(String strategy, String searchValue) =>
_post('elements', { 'using': strategy, 'value' : searchValue });
/**
* Get the element on the page that currently has focus. The element will
* be returned as a WebElement JSON object.
*
* Potential Errors: NoSuchWindow.
*/
Future<String> getElementWithFocus() => _post('element/active');
/**
* Search for an element on the page, starting from element with id [id].
* The located element will be returned as WebElement JSON objects. See
* [findElement] for the locator strategies that each server supports.
*
* Potential Errors: NoSuchWindow, XPathLookupError.
*/
Future<String>
findElementFromId(String id, String strategy, String searchValue) {
_post('element/$id/element', { 'using': strategy, 'value' : searchValue });
}
/**
* Search for multiple elements on the page, starting from the element with
* id [id].The located elements will be returned as WebElement JSON objects.
* See [findElement] for the locator strategies that each server supports.
* Elements are be returned in the order located in the DOM.
*
* Potential Errors: NoSuchWindow, XPathLookupError.
*/
Future<List<String>>
findElementsFromId(String id, String strategy, String searchValue) =>
_post('element/$id/elements',
params: { 'using': strategy, 'value' : searchValue });
/**
* Click on an element specified by [id].
*
* Potential Errors: NoSuchWindow, StaleElementReference, ElementNotVisible
* (if the referenced element is not visible on the page, either hidden
* by CSS, or has 0-width or 0-height).
*/
Future clickElement(String id) => _post('element/$id/click');
/**
* Submit a FORM element. The submit command may also be applied to any
* element that is a descendant of a FORM element.
*
* Potential Errors: NoSuchWindow, StaleElementReference.
*/
Future submit(String id) => _post('element/$id/submit');
/** Returns the visible text for the element.
*
* Potential Errors: NoSuchWindow, StaleElementReference.
*/
Future<String> getElementText(String id) => _get('element/$id/text');
/**
* Send a sequence of key strokes to an element.
*
* Any UTF-8 character may be specified, however, if the server does not
* support native key events, it will simulate key strokes for a standard
* US keyboard layout. The Unicode Private Use Area code points,
* 0xE000-0xF8FF, are used to represent pressable, non-text keys:
*
* NULL - U+E000
*
* Cancel - U+E001
*
* Help - U+E002
*
* Backspace - U+E003
*
* Tab - U+E004
*
* Clear - U+E005
*
* Return - U+E006
*
* Enter - U+E007
*
* Shift - U+E008
*
* Control - U+E009
*
* Alt - U+E00A
*
* Pause - U+E00B
*
* Escape - U+E00C
*
* Space - U+E00D
*
* Pageup - U+E00E
*
* Pagedown - U+E00F
*
* End - U+E010
*
* Home - U+E011
*
* Left arrow - U+E012
*
* Up arrow - U+E013
*
* Right arrow - U+E014
*
* Down arrow - U+E015
*
* Insert - U+E016
*
* Delete - U+E017
*
* Semicolon - U+E018
*
* Equals - U+E019
*
* Numpad 0..9 - U+E01A..U+E023
*
* Multiply - U+E024
*
* Add - U+E025
*
* Separator - U+E026
*
* Subtract - U+E027
*
* Decimal - U+E028
*
* Divide - U+E029
*
* F1..F12 - U+E031..U+E03C
*
* Command/Meta U+E03D
*
* The server processes the key sequence as follows:
*
* - Each key that appears on the keyboard without requiring modifiers is
* sent as a keydown followed by a key up.
*
* - If the server does not support native events and must simulate key
* strokes with JavaScript, it will generate keydown, keypress, and keyup
* events, in that order. The keypress event is only fired when the
* corresponding key is for a printable character.
*
* - If a key requires a modifier key (e.g. "!" on a standard US keyboard),
* the sequence is: modifier down, key down, key up, modifier up, where
* key is the ideal unmodified key value (using the previous example,
* a "1").
*
* - Modifier keys (Ctrl, Shift, Alt, and Command/Meta) are assumed to be
* "sticky"; each modifier is held down (e.g. only a keydown event) until
* either the modifier is encountered again in the sequence, or the NULL
* (U+E000) key is encountered.
*
* - Each key sequence is terminated with an implicit NULL key.
* Subsequently, all depressed modifier keys are released (with
* corresponding keyup events) at the end of the sequence.
*
* Potential Errors: NoSuchWindow, StaleElementReference, ElementNotVisible.
*/
Future sendKeyStrokesToElement(String id, List<String> keys) =>
_post('element/$id/value', { 'value': keys });
/**
* Send a sequence of key strokes to the active element. This command is
* similar to [sendKeyStrokesToElement] command in every aspect except the
* implicit termination: The modifiers are not released at the end of the
* call. Rather, the state of the modifier keys is kept between calls,
* so mouse interactions can be performed while modifier keys are depressed.
*
* Potential Errors: NoSuchWindow.
*/
Future sendKeyStrokes(List<String> keys) => _post('keys', { 'value': keys });
/**
* Query for an element's tag name, as a lower-case string.
*
* Potential Errors: NoSuchWindow, StaleElementReference.
*/
Future<String> getElementTagName(String id) => _get('element/$id/name');
/**
* Clear a TEXTAREA or text INPUT element's value.
*
* Potential Errors: NoSuchWindow, StaleElementReference, ElementNotVisible,
* InvalidElementState.
*/
Future clearValue(String id) => _post('/element/$id/clear');
/**
* Determine if an OPTION element, or an INPUT element of type checkbox
* or radiobutton is currently selected.
*
* Potential Errors: NoSuchWindow, StaleElementReference.
*/
Future<bool> isSelected(String id) => _get('element/$id/selected');
/**
* Determine if an element is currently enabled.
*
* Potential Errors: NoSuchWindow, StaleElementReference.
*/
Future<bool> isEnabled(String id) => _get('element/$id/enabled');
/**
* Get the value of an element's attribute, or null if it has no such
* attribute.
*
* Potential Errors: NoSuchWindow, StaleElementReference.
*/
Future<String> getAttribute(String id, String attribute) =>
_get('element/$id/attribute/$attribute');
/**
* Test if two element IDs refer to the same DOM element.
*
* Potential Errors: NoSuchWindow, StaleElementReference.
*/
Future<bool> areSameElement(String id, String other) =>
_get('element/$id/equals/$other');
/**
* Determine if an element is currently displayed.
*
* Potential Errors: NoSuchWindow, StaleElementReference.
*/
Future<bool> isDiplayed(String id) => _get('element/$id/displayed');
/**
* Determine an element's location on the page. The point (0, 0) refers to
* the upper-left corner of the page. The element's coordinates are returned
* as a [Map] object with x and y properties.
*
* Potential Errors: NoSuchWindow, StaleElementReference.
*/
Future<Map> getElementLocation(String id) => _get('element/$id/location');
/**
* Determine an element's size in pixels. The size will be returned as a
* [Map] object with width and height properties.
*
* Potential Errors: NoSuchWindow, StalElementReference.
*/
Future<Map> getElementSize(String id) => _get('element/$id/size');
/**
* Query the value of an element's computed CSS property. The CSS property
* to query should be specified using the CSS property name, not the
* JavaScript property name (e.g. background-color instead of
* backgroundColor).
*
* Potential Errors: NoSuchWindow, StaleElementReference.
*/
Future<String> getElementCssProperty(String id, String property) =>
_get('element/$id/css/$property');
/**
* Get the current browser orientation ('LANDSCAPE' or 'PORTRAIT').
*
* Potential Errors: NoSuchWindow.
*/
Future<String> getBrowserOrientation() => _get('orientation');
/**
* Gets the text of the currently displayed JavaScript alert(), confirm(),
* or prompt() dialog.
*
* Potential Errors: NoAlertPresent.
*/
Future<String> getAlertText() => _get('alert_text');
/**
* Sends keystrokes to a JavaScript prompt() dialog.
*
* Potential Errors: NoAlertPresent.
*/
Future sendKeyStrokesToPrompt(String text) =>
_post('alert_text', { 'text': text });
/**
* Accepts the currently displayed alert dialog. Usually, this is equivalent
* to clicking on the 'OK' button in the dialog.
*
* Potential Errors: NoAlertPresent.
*/
Future acceptAlert() => _post('accept_alert');
/**
* Dismisses the currently displayed alert dialog. For confirm() and prompt()
* dialogs, this is equivalent to clicking the 'Cancel' button. For alert()
* dialogs, this is equivalent to clicking the 'OK' button.
*
* Potential Errors: NoAlertPresent.
*/
Future dismissAlert() => _post('dismiss_alert');
/**
* Move the mouse by an offset of the specificed element. If no element is
* specified, the move is relative to the current mouse cursor. If an
* element is provided but no offset, the mouse will be moved to the center
* of the element. If the element is not visible, it will be scrolled
* into view.
*/
Future moveTo(String id, int x, int y) =>
_post('moveto', { 'element': id, 'xoffset': x, 'yoffset' : y});
/**
* Click a mouse button (at the coordinates set by the last [moveTo] command).
* Note that calling this command after calling [buttonDown] and before
* calling [buttonUp] (or any out-of-order interactions sequence) will yield
* undefined behaviour).
*
* [button] should be 0 for left, 1 for middle, or 2 for right.
*/
Future clickMouse([button = 0]) => _post('click', { 'button' : button });
/**
* Click and hold the left mouse button (at the coordinates set by the last
* [moveTo] command). Note that the next mouse-related command that should
* follow is [buttonDown]. Any other mouse command (such as [click] or
* another call to [buttonDown]) will yield undefined behaviour.
*
* [button] should be 0 for left, 1 for middle, or 2 for right.
*/
Future buttonDown([button = 0]) => _post('click', { 'button' : button });
/**
* Releases the mouse button previously held (where the mouse is currently
* at). Must be called once for every [buttonDown] command issued. See the
* note in [click] and [buttonDown] about implications of out-of-order
* commands.
*
* [button] should be 0 for left, 1 for middle, or 2 for right.
*/
Future buttonUp([button = 0]) => _post('click', { 'button' : button });
/** Double-clicks at the current mouse coordinates (set by [moveTo]). */
Future doubleClick() => _post('doubleclick');
/** Single tap on the touch enabled device on the element with id [id]. */
Future touchClick(String id) => _post('touch/click', { 'element': id });
/** Finger down on the screen. */
Future touchDown(int x, int y) => _post('touch/down', { 'x': x, 'y': y });
/** Finger up on the screen. */
Future touchUp(int x, int y) => _post('touch/up', { 'x': x, 'y': y });
/** Finger move on the screen. */
Future touchMove(int x, int y) => _post('touch/move', { 'x': x, 'y': y });
/**
* Scroll on the touch screen using finger based motion events. If [id] is
* specified, scrolling will start at a particular screen location.
*/
Future touchScroll(int xOffset, int yOffset, [String id = null]) {
if (id == null) {
return _post('touch/scroll', { 'xoffset': xOffset, 'yoffset': yOffset });
} else {
return _post('touch/scroll',
{ 'element': id, 'xoffset': xOffset, 'yoffset': yOffset });
}
}
/** Double tap on the touch screen using finger motion events. */
Future touchDoubleClick(String id) =>
_post('touch/doubleclick', { 'element': id });
/** Long press on the touch screen using finger motion events. */
Future touchLongClick(String id) =>
_post('touch/longclick', { 'element': id });
/**
* Flick on the touch screen using finger based motion events, starting
* at a particular screen location. [speed] is in pixels-per-second.
*/
Future touchFlickFrom(String id, int xOffset, int yOffset, int speed) =>
_post('touch/flick',
{ 'element': id, 'xoffset': xOffset, 'yoffset': yOffset,
'speed': speed });
/**
* Flick on the touch screen using finger based motion events. Use this
* instead of [touchFlickFrom] if you don'tr care where the flick starts.
*/
Future touchFlick(int xSpeed, int ySpeed) =>
_post('touch/flick', { 'xSpeed': xSpeed, 'ySpeed': ySpeed });
/**
* Get the current geo location. Returns a [Map] with latitude,
* longitude and altitude properties.
*/
Future<Map> getGeolocation() => _get('location');
/** Set the current geo location. */
Future setLocation(double latitude, double longitude, double altitude) =>
_post('location',
{ 'latitude': latitude,
'longitude': longitude,
'altitude': altitude });
/**
* Only a few drivers actually support the JSON storage commands.
* Currently it looks like this is the Android and iPhone drivers only.
* For the rest, we can achieve a similar effect with Javascript
* execution. The flag below is used to control whether to do this.
*/
bool useJavascriptForStorageAPIs = true;
/**
* Get all keys of the local storage. Completes with [null] if there
* are no keys or the keys could not be retrieved.
*
* Potential Errors: NoSuchWindow.
*/
Future<List<String>> getLocalStorageKeys() {
if (useJavascriptForStorageAPIs) {
return execute(
'var rtn = [];'
'for (var i = 0; i < window.localStorage.length; i++)'
' rtn.push(window.localStorage.key(i));'
'return rtn;');
} else {
return _get('local_storage');
}
}
/**
* Set the local storage item for the given key.
*
* Potential Errors: NoSuchWindow.
*/
Future setLocalStorageItem(String key, String value) {
if (useJavascriptForStorageAPIs) {
return execute('window.localStorage.setItem(arguments[0], arguments[1]);',
[key, value]);
} else {
return _post('local_storage', { 'key': key, 'value': value });
}
}
/**
* Clear the local storage.
*
* Potential Errors: NoSuchWindow.
*/
Future clearLocalStorage() {
if (useJavascriptForStorageAPIs) {
return execute('return window.localStorage.clear();');
} else {
return _delete('local_storage');
}
}
/**
* Get the local storage item for the given key.
*
* Potential Errors: NoSuchWindow.
*/
Future<String> getLocalStorageValue(String key) {
if (useJavascriptForStorageAPIs) {
return execute('return window.localStorage.getItem(arguments[0]);',
[key]);
} else {
return _get('local_storage/key/$key');
}
}
/**
* Delete the local storage item for the given key.
*
* Potential Errors: NoSuchWindow.
*/
Future deleteLocalStorageValue(String key) {
if (useJavascriptForStorageAPIs) {
return execute('return window.localStorage.removeItem(arguments[0]);',
[key]);
} else {
return _delete('local_storage/key/$key');
}
}
/**
* Get the number of items in the local storage.
*
* Potential Errors: NoSuchWindow.
*/
Future<int> getLocalStorageCount() {
if (useJavascriptForStorageAPIs) {
return execute('return window.localStorage.length;');
} else {
return _get('local_storage/size');
}
}
/**
* Get all keys of the session storage.
*
* Potential Errors: NoSuchWindow.
*/
Future<List<String>> getSessionStorageKeys() {
if (useJavascriptForStorageAPIs) {
return execute(
'var rtn = [];'
'for (var i = 0; i < window.sessionStorage.length; i++)'
' rtn.push(window.sessionStorage.key(i));'
'return rtn;');
} else {
return _get('session_storage');
}
}
/**
* Set the sessionstorage item for the given key.
*
* Potential Errors: NoSuchWindow.
*/
Future setSessionStorageItem(String key, String value) {
if (useJavascriptForStorageAPIs) {
return execute(
'window.sessionStorage.setItem(arguments[0], arguments[1]);',
[key, value]);
} else {
return _post('session_storage', { 'key': key, 'value': value });
}
}
/**
* Clear the session storage.
*
* Potential Errors: NoSuchWindow.
*/
Future clearSessionStorage() {
if (useJavascriptForStorageAPIs) {
return execute('window.sessionStorage.clear();');
} else {
return _delete('session_storage');
}
}
/**
* Get the session storage item for the given key.
*
* Potential Errors: NoSuchWindow.
*/
Future<String> getSessionStorageValue(String key) {
if (useJavascriptForStorageAPIs) {
return execute('return window.sessionStorage.getItem(arguments[0]);',
[key]);
} else {
return _get('session_storage/key/$key');
}
}
/**
* Delete the session storage item for the given key.
*
* Potential Errors: NoSuchWindow.
*/
Future deleteSessionStorageValue(String key) {
if (useJavascriptForStorageAPIs) {
return execute('return window.sessionStorage.removeItem(arguments[0]);',
[key]);
} else {
return _delete('session_storage/key/$key');
}
}
/**
* Get the number of items in the session storage.
*
* Potential Errors: NoSuchWindow.
*/
Future<String> getSessionStorageCount() {
if (useJavascriptForStorageAPIs) {
return execute('return window.sessionStorage.length;');
} else {
return _get('session_storage/size');
}
}
/**
* Get available log types ('client', 'driver', 'browser', 'server').
* This works with Firefox but Chrome returns a 500 response due to a
* bad cast.
*/
Future<List<String>> getLogTypes() => _get('log/types');
/**
* Get the log for a given log type. Log buffer is reset after each request.
* Each log entry is a [Map] with these fields:
*
* 'timestamp' (int) - The timestamp of the entry.
* 'level' (String) - The log level of the entry, for example, "INFO".
* 'message' (String) - The log message.
*
* This works with Firefox but Chrome returns a 500 response due to a
* bad cast.
*/
Future<List<Map>> getLogs(String type) => _post('log', { 'type': type });
}