Merge pull request #73 from DrMarcII/master

Add support/forwarder.dart
diff --git a/lib/html.dart b/lib/html.dart
index f5f9e6e..56ce288 100644
--- a/lib/html.dart
+++ b/lib/html.dart
@@ -18,7 +18,7 @@
 import 'dart:convert' show JSON, UTF8;
 import 'dart:html' show HttpRequest, ProgressEvent;
 
-import 'package:webdriver/async_helpers.dart' show Lock;
+import 'package:webdriver/support/async.dart' show Lock;
 import 'package:webdriver/core.dart' as core
     show createDriver, fromExistingSession, WebDriver;
 import 'package:webdriver/src/command_processor.dart' show CommandProcessor;
diff --git a/lib/io.dart b/lib/io.dart
index d20dceb..a5fa975 100644
--- a/lib/io.dart
+++ b/lib/io.dart
@@ -24,7 +24,7 @@
         HttpClientResponse,
         HttpHeaders;
 
-import 'package:webdriver/async_helpers.dart' show Lock;
+import 'package:webdriver/support/async.dart' show Lock;
 import 'package:webdriver/core.dart' as core
     show createDriver, fromExistingSession, WebDriver;
 import 'package:webdriver/src/command_processor.dart' show CommandProcessor;
diff --git a/lib/src/web_driver.dart b/lib/src/web_driver.dart
index fc64ed1..ffe26e2 100644
--- a/lib/src/web_driver.dart
+++ b/lib/src/web_driver.dart
@@ -178,13 +178,20 @@
   }
 
   Future postRequest(String command, [params]) =>
-      _commandProcessor.post(_prefix.resolve(command), params);
+      _commandProcessor.post(_resolve(command), params);
 
-  Future getRequest(String command) =>
-      _commandProcessor.get(_prefix.resolve(command));
+  Future getRequest(String command) => _commandProcessor.get(_resolve(command));
 
   Future deleteRequest(String command) =>
-      _commandProcessor.delete(_prefix.resolve(command));
+      _commandProcessor.delete(_resolve(command));
+
+  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;
diff --git a/lib/async_helpers.dart b/lib/support/async.dart
similarity index 98%
rename from lib/async_helpers.dart
rename to lib/support/async.dart
index 8b1d311..3f021af 100644
--- a/lib/async_helpers.dart
+++ b/lib/support/async.dart
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-library webdriver.async_helpers;
+library webdriver.support.async;
 
 import 'dart:async' show Completer, Future;
 
diff --git a/lib/support/forwarder.dart b/lib/support/forwarder.dart
new file mode 100644
index 0000000..651ec8b
--- /dev/null
+++ b/lib/support/forwarder.dart
@@ -0,0 +1,214 @@
+// 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, StreamConsumer;
+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/core.dart'
+    show By, WebDriver, WebDriverException, WebElement;
+
+final _contentTypeJson =
+    new 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 forward(HttpRequest request) async {
+    try {
+      if (!request.uri.path.startsWith(prefix)) {
+        request.response.statusCode = HttpStatus.NOT_FOUND;
+        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);
+      }
+      var params;
+      if (request.method == 'POST') {
+        String requestBody = await UTF8.decodeStream(request);
+        if (requestBody != null && requestBody.isNotEmpty) {
+          params = JSON.decode(requestBody);
+        }
+      }
+      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 _forward(String method, String endpoint,
+      [Map<String, 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 =
+              new File(path.join(outputDir.path, params['file'])).openWrite();
+          await driver.captureScreenshot().pipe(file as StreamConsumer<int>);
+          return null;
+        }
+        break;
+      case 'source':
+        if (method == 'POST') {
+          // grab page source and save to file system
+          await new 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 = new 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);
+        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(new 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;
+    }
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index fd8dba8..4c661d8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: webdriver
-version: 0.10.0-pre.7
+version: 0.10.0-pre.8
 author: Marc Fisher II <fisherii@google.com>
 description: >
   Provides WebDriver bindings for Dart. These use the WebDriver JSON interface,
diff --git a/test/frame.html b/test/frame.html
index 30569eb..f1c7f09 100644
--- a/test/frame.html
+++ b/test/frame.html
@@ -17,11 +17,11 @@
 -->
 
 <html>
-  <head>
+<head>
     <title>frame</title>
-  </head>
+</head>
 
-  <body>
-    <p id="text">this is a frame</p>
-  </body>
+<body>
+<p id="text">this is a frame</p>
+</body>
 </html>
diff --git a/test/io_config.dart b/test/io_config.dart
new file mode 100644
index 0000000..596d75b
--- /dev/null
+++ b/test/io_config.dart
@@ -0,0 +1,58 @@
+// 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.io_test;
+
+import 'dart:io' show FileSystemEntity, Platform;
+
+import 'package:path/path.dart' as path;
+import 'package:webdriver/io.dart' show WebDriver, Capabilities, createDriver;
+
+import 'test_util.dart' as test_util;
+
+void config() {
+  test_util.runningOnTravis = Platform.environment['TRAVIS'] == 'true';
+  test_util.createTestDriver = ({Map additionalCapabilities}) {
+    Map capabilities = Capabilities.chrome;
+    Map env = Platform.environment;
+
+    Map chromeOptions = {};
+
+    if (env['CHROMEDRIVER_BINARY'] != null) {
+      chromeOptions['binary'] = env['CHROMEDRIVER_BINARY'];
+    }
+
+    if (env['CHROMEDRIVER_ARGS'] != null) {
+      chromeOptions['args'] = env['CHROMEDRIVER_ARGS'].split(' ');
+    }
+
+    if (chromeOptions.isNotEmpty) {
+      capabilities['chromeOptions'] = chromeOptions;
+    }
+
+    if (additionalCapabilities != null) {
+      capabilities.addAll(additionalCapabilities);
+    }
+
+    return createDriver(desired: capabilities);
+  };
+
+  var testPagePath = path.join(path.current, 'test', 'test_page.html');
+  testPagePath = path.absolute(testPagePath);
+  if (!FileSystemEntity.isFileSync(testPagePath)) {
+    throw new Exception('Could not find the test file at "$testPagePath".'
+        ' Make sure you are running tests from the root of the project.');
+  }
+  test_util.testPagePath = path.toUri(testPagePath).toString();
+}
diff --git a/test/io_test.dart b/test/io_test.dart
index 68feb35..62d2768 100644
--- a/test/io_test.dart
+++ b/test/io_test.dart
@@ -15,12 +15,7 @@
 @TestOn("vm")
 library webdriver.io_test;
 
-import 'dart:io' show FileSystemEntity, Platform;
-
-import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
-import 'package:webdriver/io.dart'
-    show WebDriver, Capabilities, createDriver, fromExistingSession;
 
 import 'src/alert.dart' as alert;
 import 'src/keyboard.dart' as keyboard;
@@ -33,64 +28,10 @@
 import 'src/web_element.dart' as web_element;
 import 'src/window.dart' as window;
 
-import 'test_util.dart' as test_util;
+import 'io_config.dart' as config;
 
 void main() {
-  test_util.runningOnTravis = Platform.environment['TRAVIS'] == 'true';
-  test_util.createTestDriver = ({Map additionalCapabilities}) {
-    Map capabilities = Capabilities.chrome;
-    Map env = Platform.environment;
-
-    Map chromeOptions = {};
-
-    if (env['CHROMEDRIVER_BINARY'] != null) {
-      chromeOptions['binary'] = env['CHROMEDRIVER_BINARY'];
-    }
-
-    if (env['CHROMEDRIVER_ARGS'] != null) {
-      chromeOptions['args'] = env['CHROMEDRIVER_ARGS'].split(' ');
-    }
-
-    if (chromeOptions.isNotEmpty) {
-      capabilities['chromeOptions'] = chromeOptions;
-    }
-
-    if (additionalCapabilities != null) {
-      capabilities.addAll(additionalCapabilities);
-    }
-
-    return createDriver(desired: capabilities);
-  };
-
-  var testPagePath = path.join(path.current, 'test', 'test_page.html');
-  testPagePath = path.absolute(testPagePath);
-  if (!FileSystemEntity.isFileSync(testPagePath)) {
-    throw new Exception('Could not find the test file at "$testPagePath".'
-        ' Make sure you are running tests from the root of the project.');
-  }
-  test_util.testPagePath = path.toUri(testPagePath).toString();
-
-  group('io-specific tests', () {
-    WebDriver driver;
-    setUp(() async {
-      driver = await test_util.createTestDriver();
-      await driver.get(test_util.testPagePath);
-    });
-
-    tearDown(() => driver.quit());
-
-    test('fromExistingSession', () async {
-      WebDriver newDriver =
-          await fromExistingSession(driver.id, uri: driver.uri);
-      expect(newDriver.capabilities, driver.capabilities);
-      var url = await newDriver.currentUrl;
-      expect(url, startsWith('file:'));
-      expect(url, endsWith('test_page.html'));
-      await newDriver.get('http://www.google.com/ncr');
-      url = await driver.currentUrl;
-      expect(url, contains('www.google.com'));
-    });
-  });
+  config.config();
 
   alert.runTests();
   keyboard.runTests();
diff --git a/test/src/navigation.dart b/test/src/navigation.dart
index 3ab0ff7..388e843 100644
--- a/test/src/navigation.dart
+++ b/test/src/navigation.dart
@@ -15,7 +15,7 @@
 library webdriver.navigation_test;
 
 import 'package:test/test.dart';
-import 'package:webdriver/async_helpers.dart';
+import 'package:webdriver/support/async.dart';
 import 'package:webdriver/core.dart';
 
 import '../test_util.dart';
diff --git a/test/src/window.dart b/test/src/window.dart
index 5b0c94a..0b60fe0 100644
--- a/test/src/window.dart
+++ b/test/src/window.dart
@@ -17,7 +17,7 @@
 import 'dart:math' show Point, Rectangle;
 
 import 'package:test/test.dart';
-import 'package:webdriver/async_helpers.dart';
+import 'package:webdriver/support/async.dart';
 import 'package:webdriver/core.dart';
 
 import '../test_util.dart';
diff --git a/test/async_helpers_test.dart b/test/support/async_test.dart
similarity index 97%
rename from test/async_helpers_test.dart
rename to test/support/async_test.dart
index 21c8239..b33a858 100644
--- a/test/async_helpers_test.dart
+++ b/test/support/async_test.dart
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-library webdriver.async_helpers_test;
+library webdriver.support.async_test;
 
 import 'dart:async' show Future;
 
 import 'package:test/test.dart';
-import 'package:webdriver/async_helpers.dart';
+import 'package:webdriver/support/async.dart';
 
 void main() {
   group('Lock', () {
diff --git a/test/support/forwarder_test.dart b/test/support/forwarder_test.dart
new file mode 100644
index 0000000..a6afbeb
--- /dev/null
+++ b/test/support/forwarder_test.dart
@@ -0,0 +1,157 @@
+// 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.
+
+@TestOn("vm")
+library webdriver.support.forwarder_test;
+
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+import 'package:test/test.dart';
+import 'package:webdriver/io.dart';
+import 'package:webdriver/support/forwarder.dart';
+
+import '../io_config.dart' as config;
+import '../test_util.dart' as test_util;
+
+const buttonClicked = 'Button clicked';
+const buttonNotClicked = 'Button not clicked';
+
+void main() {
+  config.config();
+
+  group('WebDriverForwarder', () {
+    WebDriver driver;
+    WebDriverForwarder forwarder;
+    HttpServer server;
+    WebDriver forwardedDriver;
+    Uri address;
+
+    setUp(() async {
+      driver = await test_util.createTestDriver();
+      ;
+      forwarder =
+          new WebDriverForwarder(driver, prefix: '/webdriver/session/1');
+
+      server = await HttpServer.bind(InternetAddress.ANY_IP_V4, 0);
+      server.listen((request) {
+        if (request.uri.path.startsWith('/webdriver')) {
+          forwarder.forward(request);
+        } else if (request.method == 'GET' &&
+            request.uri.path.endsWith('test_page.html')) {
+          File file = new File(
+              path.join('test', 'support', 'forwarder_test_page.html'));
+          request.response
+            ..statusCode = HttpStatus.OK
+            ..headers.set('Content-type', 'text/html');
+          file.openRead().pipe(request.response);
+        } else {
+          request.response
+            ..statusCode = HttpStatus.NOT_FOUND
+            ..close();
+        }
+      });
+      address = new Uri.http('localhost:${server.port}', '/webdriver/');
+      forwardedDriver = await fromExistingSession('1', uri: address);
+
+      await forwardedDriver.get(address.resolve('/test_page.html'));
+    });
+
+    tearDown(() async {
+      try {
+        await forwardedDriver.quit();
+      } catch (e) {
+        print('Ignored error quitting forwardedDriver: $e');
+      }
+      try {
+        await server.close(force: true);
+      } catch (e) {
+        print('Ignored error quitting server: $e');
+      }
+      try {
+        await driver.quit();
+      } catch (e) {
+        print('Ignored error quitting driver: $e');
+      }
+    });
+
+    test('get url', () async {
+      expect(await forwardedDriver.currentUrl, endsWith('test_page.html'));
+    });
+
+    test('click button', () async {
+      expect(await forwardedDriver.getRequest('element/div/text'),
+          buttonNotClicked);
+
+      await forwardedDriver.postRequest('element/button/click', {'button': 0});
+      expect(
+          await forwardedDriver.getRequest('element/div/text'), buttonClicked);
+    });
+
+    test('moveto/click', () async {
+      expect(await forwardedDriver.getRequest('element/div/text'),
+          buttonNotClicked);
+
+      await forwardedDriver.postRequest('moveto', {'element': 'button'});
+      await forwardedDriver.mouse.click();
+
+      expect(
+          await forwardedDriver.getRequest('element/div/text'), buttonClicked);
+    });
+
+    test('execute_script', () async {
+      expect(await forwardedDriver.getRequest('element/div/text'),
+          buttonNotClicked);
+
+      await forwardedDriver.execute(
+          'arguments[0].el.click();', [{'el': {'ELEMENT': 'button'}}]);
+
+      expect(
+          await forwardedDriver.getRequest('element/div/text'), buttonClicked);
+    });
+
+    test('element equals', () async {
+      expect(
+          await forwardedDriver.getRequest('element/div/equals/div'), isTrue);
+      expect(await forwardedDriver.getRequest('element/div/equals/button'),
+          isFalse);
+    });
+
+    // TODO(DrMarcII) add test that actually uses shadow dom
+    test('enable/disable deep', () async {
+      await forwardedDriver.postRequest('disabledeep');
+
+      expect(await forwardedDriver.getRequest('element/div/text'),
+          buttonNotClicked);
+
+      await forwardedDriver.postRequest('element/button/click', {'button': 0});
+      expect(
+          await forwardedDriver.getRequest('element/div/text'), buttonClicked);
+
+      await forwardedDriver.postRequest('enabledeep');
+      await forwardedDriver.navigate.refresh();
+
+      expect(await forwardedDriver.getRequest('element/div/text'),
+          buttonNotClicked);
+
+      await forwardedDriver.postRequest('element/button/click', {'button': 0});
+      expect(
+          await forwardedDriver.getRequest('element/div/text'), buttonClicked);
+    });
+
+    test('window close', () async {
+      await forwardedDriver.close();
+    });
+  });
+}
diff --git a/test/support/forwarder_test_page.html b/test/support/forwarder_test_page.html
new file mode 100644
index 0000000..0991503
--- /dev/null
+++ b/test/support/forwarder_test_page.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+
+<!--
+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.
+-->
+
+<html>
+<head lang="en">
+    <meta charset="UTF-8">
+    <title></title>
+</head>
+<body>
+
+<input type="button" wd-element-id="button" onclick="buttonClicked()" value="button">
+
+<div wd-element-id="div">Button not clicked</div>
+
+<script>
+    function buttonClicked() {
+        document.querySelector('[wd-element-id="div"]').textContent = 'Button clicked';
+    }
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/test/test_page.html b/test/test_page.html
index ba9e7e7..ec05a7c 100644
--- a/test/test_page.html
+++ b/test/test_page.html
@@ -17,42 +17,43 @@
 -->
 
 <html>
-  <head>
+<head>
     <title>test_page</title>
     <script>
-      function promptForText() {
-        var val = prompt('button clicked');
-        if (val == null) {
-          document.getElementById('settable').textContent = 'dismissed';
-        } else {
-          document.getElementById('settable').textContent = 'accepted ' + val;
+        function promptForText() {
+            var val = prompt('button clicked');
+            if (val == null) {
+                document.getElementById('settable').textContent = 'dismissed';
+            } else {
+                document.getElementById('settable').textContent = 'accepted ' + val;
+            }
         }
-      }
     </script>
-  </head>
+</head>
 
-  <body>
-    <table id='table1' non-standard='a non standard attr'>
-      <tr>
+<body>
+<table id='table1' non-standard='a non standard attr'>
+    <tr>
         <td>r1c1</td>
         <td>r1c2</td>
-      </tr>
-      <tr>
+    </tr>
+    <tr>
         <td>r2c1</td>
         <td>r2c2</td>
-      </tr>
-    </table>
-    <button onclick='promptForText();'>button</button>
-    <form onsubmit='alert("form submitted")'>
-      <input type='text' />
-      <input type='checkbox' />
-      <input type='password' disabled />
-    </form>
-    <div id='div' style='display: none; background-color: red'>
-      some not displayed text</div>
-    <a id='settable' href="test_page.html" target="_new">
-      Open copy in other window</a>
-    <br />
-    <iframe src="frame.html" name="frame" width="100%" />
-  </body>
+    </tr>
+</table>
+<button onclick='promptForText();'>button</button>
+<form onsubmit='alert("form submitted")'>
+    <input type='text'/>
+    <input type='checkbox'/>
+    <input type='password' disabled/>
+</form>
+<div id='div' style='display: none; background-color: red'>
+    some not displayed text
+</div>
+<a id='settable' href="test_page.html" target="_new">
+    Open copy in other window</a>
+<br/>
+<iframe src="frame.html" name="frame" width="100%"/>
+</body>
 </html>
diff --git a/tool/travis.sh b/tool/travis.sh
index 1f41e17..c427b2e 100755
--- a/tool/travis.sh
+++ b/tool/travis.sh
@@ -18,15 +18,8 @@
 set -e
 
 # Verify that the libraries are error free.
-dartanalyzer --fatal-warnings \
-  lib/async_helpers.dart \
-  lib/core.dart \
-  lib/html.dart \
-  lib/io.dart \
-  test/async_helpers_test.dart \
-  test/html_test.dart \
-  test/io_test.dart
-
+grep -Rl --include "*.dart" --exclude-dir="packages" '^library .*;$' lib/ test/ | \
+    xargs dartanalyzer --fatal-warnings
 
 # Start chromedriver.
 chromedriver --port=4444 --url-base=wd/hub &
@@ -35,4 +28,3 @@
 # TODO(DrMarcII) enable running tests in browser when chrome setuid problem
 # is fixed on travis.
 pub run test -r expanded -p vm
-