| // 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; |
| } |
| } |
| } |