blob: dc4493828747f4b3eafb2c5ed723f4fc7f962a12 [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.
library webdriver.support.forwarder;
import 'dart:async' show Future;
import 'dart:convert' show json, utf8;
import 'dart:io' show ContentType, Directory, File, HttpRequest, HttpStatus;
import 'package:path/path.dart' as path;
import 'package:webdriver/async_core.dart'
show By, WebDriver, WebDriverException;
final _contentTypeJson = ContentType('application', 'json', charset: 'utf-8');
/// Attribute on elements used to locate them on passed WebDriver commands.
const wdElementIdAttribute = 'wd-element-id';
/// [WebDriverForwarder] accepts [HttpRequest]s corresponding to a variation on
/// the WebDriver wire protocol and forwards them to a WebDriver instance.
///
/// The primary difference between this and the standard wire protocol is in
/// the use of WebElement ids. When you need to refer to an element in a request
/// (URI or JSON body), then you should add an 'wd-element-id' attribute to the
/// corresponding element with a unique identifier, and use that identifier as
/// the element id for that element. This class will then search for the
/// corresponding element and in the document and will substitute an actual
/// WebElement id for the given identifier in the request.
///
/// This forwarder supports two additional commands that control how it searches
/// for elements:
/// POST '/enabledeep': enables searching through all Shadow DOMs in the
/// document for the corresponding element (but will fail on browsers that
/// don't support the '/deep/' css selector combinator).
/// POST '/disabledeep': disables searching in Shadow DOMs of the document.
///
/// This forwarder also supports two additional commands for grabbing the
/// browser contents and saving it to the file system.
/// POST '/screenshot': takes a 'file' arg and will capture a screenshot
/// of the browser and save it to the specified file name in [outputDir].
/// POST '/source': takes a 'file' arg and will capture the current page's
/// source and save it to the specified file name in [outputDir].
///
/// See https://code.google.com/p/selenium/wiki/JsonWireProtocol for
/// documentation of other commands.
class WebDriverForwarder {
/// [WebDriver] instance to forward commands to.
final WebDriver driver;
/// Path prefix that all forwarded commands will have.
final Pattern prefix;
/// Directory to save screenshots and page source to.
final Directory outputDir;
/// Search for elements in all shadow doms of the current document.
bool useDeep;
WebDriverForwarder(this.driver,
{this.prefix = '/webdriver', Directory outputDir, this.useDeep = false})
: this.outputDir = outputDir == null
? Directory.systemTemp.createTempSync()
: outputDir;
/// Forward [request] to [driver] and respond to the request with the returned
/// value or any thrown exceptions.
Future<void> forward(HttpRequest request) async {
try {
if (!request.uri.path.startsWith(prefix)) {
request.response.statusCode = HttpStatus.notFound;
return;
}
request.response.statusCode = HttpStatus.ok;
request.response.headers.contentType = _contentTypeJson;
var endpoint = request.uri.path.replaceFirst(prefix, '');
if (endpoint.startsWith('/')) {
endpoint = endpoint.substring(1);
}
Map<dynamic, dynamic> params;
if (request.method == 'POST') {
String requestBody = await utf8.decodeStream(request.cast<List<int>>());
if (requestBody != null && requestBody.isNotEmpty) {
params = json.decode(requestBody) as Map<dynamic, dynamic>;
}
}
var value = await _forward(request.method, endpoint, params);
request.response
.add(utf8.encode(json.encode({'status': 0, 'value': value})));
} on WebDriverException catch (e) {
request.response.add(utf8.encode(json.encode({
'status': e.statusCode,
'value': {'message': e.message}
})));
} catch (e) {
request.response.add(utf8.encode(json.encode({
'status': 13,
'value': {'message': e.toString()}
})));
} finally {
await request.response.close();
}
}
Future<dynamic> _forward(String method, String endpoint,
[Map<dynamic, dynamic> params]) async {
List<String> endpointTokens = path.split(endpoint);
if (endpointTokens.isEmpty) {
endpointTokens = [''];
}
switch (endpointTokens[0]) {
case 'enabledeep':
// turn on Shadow DOM support, don't forward
useDeep = true;
return null;
case 'disabledeep':
// turn off Shadow DOM support, don't forward
useDeep = false;
return null;
case 'screenshot':
if (method == 'POST') {
// take a screenshot and save to file system
var file = File(path.join(outputDir.path, params['file']));
await file.writeAsBytes(await driver.captureScreenshotAsList());
return null;
}
break;
case 'source':
if (method == 'POST') {
// grab page source and save to file system
await File(path.join(outputDir.path, params['file']))
.writeAsString(await driver.pageSource);
return null;
}
break;
case 'element':
// process endpoints of the form /element/[id]/...
if (endpointTokens.length >= 2) {
endpointTokens[1] = await _findElement(endpointTokens[1]);
}
// process endpoint /element/[id]/equals/[id]
if (endpointTokens.length == 4 && endpointTokens[2] == 'equals') {
endpointTokens[3] = await _findElement(endpointTokens[3]);
}
break;
case 'touch':
case 'moveto':
// several /touch/... endpoints and the /moveto endpoint have an
// optional 'element' param with a WebElement id value
if (params['element'] != null) {
params = Map.from(params);
params['element'] = await _findElement(params['element']);
}
break;
case 'execute':
case 'execute_async':
// /execute and /execute_async allow arbitrary JSON objects with
// embedded WebElememt ids.
params = await _deepCopy(params) as Map<dynamic, dynamic>;
break;
}
switch (method) {
case 'GET':
return await driver.getRequest(path.joinAll(endpointTokens));
case 'DELETE':
return await driver.deleteRequest(path.joinAll(endpointTokens));
case 'POST':
return await driver.postRequest(path.joinAll(endpointTokens), params);
default:
throw 'unsupported method $method';
}
}
Future<String> _findElement(String id) async {
var selector = "[$wdElementIdAttribute='$id']";
if (useDeep) {
selector = '* /deep/ $selector';
}
var elements = await driver.findElements(By.cssSelector(selector)).toList();
return elements.single.id;
}
dynamic _deepCopy(dynamic source) async {
if (source is Map) {
var copy = {};
for (var key in source.keys) {
var value = source[key];
if (key == 'ELEMENT') {
copy['ELEMENT'] = await _findElement(value);
} else {
copy[await _deepCopy(key)] = await _deepCopy(value);
}
}
return copy;
} else if (source is Iterable) {
var copy = [];
for (var value in source) {
copy.add(await _deepCopy(value));
}
return copy;
} else {
return source;
}
}
}