blob: fe65ba70a11f187bc3557ea8db91c6bbc2ad32e8 [file] [log] [blame]
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
part of webdriver.core;
typedef Future WebDriverListener(WebDriverCommandEvent event);
class WebDriver implements SearchContext {
final CommandProcessor _commandProcessor;
final Uri _prefix;
final Map<String, dynamic> capabilities;
final String id;
final Uri uri;
final bool filterStackTraces;
Stepper stepper;
/// If true, WebDriver actions are recorded as [WebDriverCommandEvent]s.
bool notifyListeners = true;
final _commandListeners = <WebDriverListener>[];
WebDriver(this._commandProcessor, this.uri, this.id, this.capabilities,
{this.filterStackTraces: true})
: this._prefix = uri.resolve('session/$id/');
/// Preferred method for registering listeners. Listeners are expected to
/// return a Future. Use new Future.value() for synchronous listeners.
void addEventListener(WebDriverListener listener) =>
_commandListeners.add(listener);
/// The current url.
Future<String> get currentUrl => getRequest<String>('url');
/// navigate to the specified url
Future get(/* Uri | String */ url) async {
if (url is Uri) {
url = url.toString();
}
await postRequest('url', {'url': url as String});
}
/// The title of the current page.
Future<String> get title => getRequest<String>('title');
/// Search for multiple elements within the entire current page.
@override
Stream<WebElement> findElements(By by) async* {
var elements = await postRequest('elements', by);
int i = 0;
for (var element in elements) {
yield new WebElement(this, element[_element], this, by, i);
i++;
}
}
/// Search for an element within the entire current page.
/// Throws [NoSuchElementException] if a matching element is not found.
@override
Future<WebElement> findElement(By by) async {
var element = await postRequest('element', by);
return new WebElement(this, element[_element], this, by);
}
/// An artist's rendition of the current page's source.
Future<String> get pageSource => getRequest<String>('source');
/// Close the current window, quitting the browser if it is the last window.
Future close() async {
await deleteRequest('window');
}
/// Quit the browser.
Future quit({bool closeSession: true}) async {
try {
if (closeSession) {
await _commandProcessor.delete(uri.resolve('session/$id'));
}
} finally {
await _commandProcessor.close();
}
}
/// Handles for all of the currently displayed tabs/windows.
Stream<Window> get windows async* {
var handles = await getRequest('window_handles');
for (var handle in handles) {
yield new Window._(this, handle);
}
}
/// Handle for the active tab/window.
Future<Window> get window async {
var handle = await getRequest('window_handle');
return new Window._(this, handle);
}
/// The currently focused element, or the body element if no element has
/// focus.
Future<WebElement> get activeElement async {
var element = await postRequest('element/active');
if (element != null) {
return new WebElement(this, element[_element], this, 'activeElement');
}
return null;
}
TargetLocator get switchTo => new TargetLocator._(this);
Navigation get navigate => new Navigation._(this);
Cookies get cookies => new Cookies._(this);
Logs get logs => new Logs._(this);
Timeouts get timeouts => new Timeouts._(this);
Keyboard get keyboard => new Keyboard._(this);
Mouse get mouse => new Mouse._(this);
/// Take a screenshot of the current page as PNG and return it as
/// base64-encoded string.
Future<String> captureScreenshotAsBase64() async =>
await getRequest('screenshot');
/// Take a screenshot of the current page as PNG as list of uint8.
Future<List<int>> captureScreenshotAsList() async {
var base64Encoded = captureScreenshotAsBase64();
return base64.decode(await base64Encoded);
}
/// 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 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 will be thrown.
///
/// 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-able object. WebElements will be converted to
/// the corresponding DOM element. Likewise, any DOM Elements in the script
/// result will be converted to WebElements.
Future executeAsync(String script, List args) =>
postRequest('execute_async', {'script': script, 'args': args})
.then(_recursiveElementify);
/// 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.
///
/// 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-able object. WebElements will be converted to
/// the corresponding DOM element. Likewise, any DOM Elements in the script
/// result will be converted to WebElements.
Future execute(String script, List args) =>
postRequest('execute', {'script': script, 'args': args})
.then(_recursiveElementify);
dynamic _recursiveElementify(result) {
if (result is Map) {
if (result.length == 1 && result.containsKey(_element)) {
return new WebElement(
this, result[_element] as String, this, 'javascript');
} else {
var newResult = {};
result.forEach((key, value) {
newResult[key] = _recursiveElementify(value);
});
return newResult;
}
} else if (result is List) {
return result.map(_recursiveElementify).toList();
} else {
return result;
}
}
Future<T> postRequest<T>(String command, [params]) =>
_performRequestWithLog<T>(
() => _commandProcessor.post(_resolve(command), params),
'POST',
command,
params);
Future<T> getRequest<T>(String command) => _performRequestWithLog<T>(
() => _commandProcessor.get(_resolve(command)), 'GET', command, null);
Future<T> deleteRequest<T>(String command) => _performRequestWithLog<T>(
() => _commandProcessor.delete(_resolve(command)),
'DELETE',
command,
null);
// Performs request and sends the result to listeners/onCommandController.
// This is typically always what you want to use.
Future<T> _performRequestWithLog<T>(
Function fn, String method, String command, params) async {
return await _performRequest<T>(fn, method, command, params)
.whenComplete(() async {
if (notifyListeners) {
if (_previousEvent == null) {
throw new Error(); // This should be impossible.
}
for (WebDriverListener listener in _commandListeners) {
await listener(_previousEvent);
}
}
});
}
// This is an ugly hack, I know, but I dunno how to cleanly do this.
WebDriverCommandEvent _previousEvent;
// Performs the request. This will not notify any listeners or
// onCommandController. This should only be called from
// _performRequestWithLog.
Future<T> _performRequest<T>(
Function fn, String method, String command, params) async {
var startTime = new DateTime.now();
var trace = new Chain.current();
if (filterStackTraces) {
trace = trace.foldFrames(
(f) => f.library.startsWith('package:webdriver/'),
terse: true);
}
Object result;
Object exception;
try {
if (stepper == null || await stepper.step(method, command, params)) {
result = await fn();
return result;
} else {
result = 'skipped';
return null;
}
} catch (e) {
exception = e;
return new Future.error(e, trace);
} finally {
if (notifyListeners) {
_previousEvent = new WebDriverCommandEvent(
method: method,
endPoint: command,
params: params,
startTime: startTime,
endTime: new DateTime.now(),
exception: exception,
result: result,
stackTrace: trace);
}
}
}
Uri _resolve(String command) {
var uri = _prefix.resolve(command);
if (uri.path.endsWith('/')) {
uri = uri.replace(path: uri.path.substring(0, uri.path.length - 1));
}
return uri;
}
@override
WebDriver get driver => this;
@override
String toString() => 'WebDriver($_prefix)';
}