Creates version of WebDriver using sync sockets.  Async remains functionally identical. (#149)

* Update WORKSPACE with recent io_bazel rules.

* Add sync-http repositories to WebDriver.

* Move files to async folder

* Add missing omit flag for sync_http.

* Initial integration of synchronous sockets into WebDriver. Currently a clone of the async interface.

* Sync version, doesn't work yet though.

* more done

* All tests migrated to sync, run, but not all pass.

* Add comments warning test does not pass (async or sync) on Mac.

* Readds VM annotation to all tests.

* Sync WebDriver moved from library based approach to import + export based approach. Allows later refactorings to merge async codebase.

* Add missing toList() calls to sync/logs.dart and sync/options.dart to ensure type safety.

* Runs dartfmt over all files.

* Update options to avoid name clash.

* Refactor sync WebDriver to delegate to rather than inherit from WebDriverBase.

* Rename WebDriverBase to Resolver.

* Update ChromeDriver version to 2.29 and driver creation to match.

* Readd wd/hub to URI.

* Try to run on official chrome.

* Update travis.yml with comments explaining Chrome / Chromium situation.

* Update travis to not use content-shell as it is deprecated.

* Remove print statements from test.

* Disable XVFB. Trying to keep travis build from stalling..

* Add comment on why XVFB not needed.

* Extend test timeouts to one minute.

* One more attempt to make travis stable.

* Reverting XVFB changess, they have no effect.
diff --git a/.travis.yml b/.travis.yml
index f7d6b1a..eee6283 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,28 +1,35 @@
 language: dart
-sudo: false
+# This is necessary to use proper Chrome. Travis's version of chromium is from
+# 2014.
+sudo: required
+dist: trusty
+addons:
+  apt:
+    sources:
+      - google-chrome
+    packages:
+      - google-chrome-stable
 
 dart:
   - dev
   - stable
 
-with_content_shell: true
+with_content_shell: false
 
 matrix:
   allow_failures:
     - dart: dev
 
 before_install:
-  - export CHROMEDRIVER_BINARY=/usr/bin/chromium-browser
+  - export CHROMEDRIVER_BINARY=/usr/bin/google-chrome
   - export CHROMEDRIVER_ARGS=--no-sandbox
-  - /usr/bin/chromium-browser --version
+  - /usr/bin/google-chrome --version
 
   - export DISPLAY=:99.0
   - sh -e /etc/init.d/xvfb start
 
 before_script:
-# We use a slightly older version of chromedriver; the newer ones require a later
-# version of chromium than is available on travis by default.
-  - wget http://chromedriver.storage.googleapis.com/2.12/chromedriver_linux64.zip
+  - wget http://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip
   - unzip chromedriver_linux64.zip
   - export PATH=$PATH:$PWD
   - ./tool/travis-setup.sh
diff --git a/lib/async_core.dart b/lib/async_core.dart
new file mode 100644
index 0000000..fdd9218
--- /dev/null
+++ b/lib/async_core.dart
@@ -0,0 +1,74 @@
+// 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.core;
+
+import 'dart:async' show Future, Stream, StreamController;
+import 'dart:collection' show UnmodifiableMapView;
+import 'dart:convert' show BASE64;
+import 'dart:math' show Point, Rectangle;
+
+import 'package:stack_trace/stack_trace.dart' show Chain;
+
+import 'package:webdriver/src/async/command_processor.dart'
+    show CommandProcessor;
+import 'package:webdriver/src/async/stepper.dart' show Stepper;
+
+export 'package:webdriver/src/async/exception.dart';
+
+part 'package:webdriver/src/async/alert.dart';
+part 'package:webdriver/src/async/capabilities.dart';
+part 'package:webdriver/src/async/command_event.dart';
+part 'package:webdriver/src/async/common.dart';
+part 'package:webdriver/src/async/keyboard.dart';
+part 'package:webdriver/src/async/logs.dart';
+part 'package:webdriver/src/async/mouse.dart';
+part 'package:webdriver/src/async/navigation.dart';
+part 'package:webdriver/src/async/options.dart';
+part 'package:webdriver/src/async/target_locator.dart';
+part 'package:webdriver/src/async/web_driver.dart';
+part 'package:webdriver/src/async/web_element.dart';
+part 'package:webdriver/src/async/window.dart';
+
+final Uri defaultUri = Uri.parse('http://127.0.0.1:4444/wd/hub/');
+
+Future<WebDriver> createDriver(CommandProcessor processor,
+    {Uri uri, Map<String, dynamic> desired}) async {
+  if (uri == null) {
+    uri = defaultUri;
+  }
+
+  if (desired == null) {
+    desired = Capabilities.empty;
+  }
+
+  Map response = await processor.post(
+      uri.resolve('session'), {'desiredCapabilities': desired},
+      value: false) as Map<String, dynamic>;
+  return new WebDriver(processor, uri, response['sessionId'],
+      new UnmodifiableMapView(response['value'] as Map<String, dynamic>));
+}
+
+Future<WebDriver> fromExistingSession(
+    CommandProcessor processor, String sessionId,
+    {Uri uri}) async {
+  if (uri == null) {
+    uri = defaultUri;
+  }
+
+  var response = await processor.get(uri.resolve('session/$sessionId'))
+      as Map<String, dynamic>;
+  return new WebDriver(
+      processor, uri, sessionId, new UnmodifiableMapView(response));
+}
diff --git a/lib/async_io.dart b/lib/async_io.dart
new file mode 100644
index 0000000..9a926c9
--- /dev/null
+++ b/lib/async_io.dart
@@ -0,0 +1,127 @@
+// 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;
+
+import 'dart:async' show Future;
+import 'dart:convert' show JSON, UTF8;
+import 'dart:io'
+    show
+        ContentType,
+        HttpClient,
+        HttpClientRequest,
+        HttpClientResponse,
+        HttpHeaders;
+
+import 'package:webdriver/core.dart' as core
+    show createDriver, fromExistingSession, WebDriver;
+import 'package:webdriver/src/async/command_processor.dart'
+    show CommandProcessor;
+import 'package:webdriver/src/async/exception.dart' show WebDriverException;
+import 'package:webdriver/support/async.dart' show Lock;
+
+export 'package:webdriver/core.dart' hide createDriver, fromExistingSession;
+
+/// Creates a WebDriver instance connected to the specified WebDriver server.
+///
+/// Note: WebDriver endpoints will be constructed using [resolve] against
+/// [uri]. Therefore, if [uri] does not end with a trailing slash, the
+/// last path component will be dropped.
+Future<core.WebDriver> createDriver({Uri uri, Map<String, dynamic> desired}) =>
+    core.createDriver(new IOCommandProcessor(), uri: uri, desired: desired);
+
+/// Creates a WebDriver instance connected to an existing session.
+///
+/// Note: WebDriver endpoints will be constructed using [resolve] against
+/// [uri]. Therefore, if [uri] does not end with a trailing slash, the
+/// last path component will be dropped.
+Future<core.WebDriver> fromExistingSession(String sessionId, {Uri uri}) =>
+    core.fromExistingSession(new IOCommandProcessor(), sessionId, uri: uri);
+
+final ContentType _contentTypeJson =
+    new ContentType("application", "json", charset: "utf-8");
+
+class IOCommandProcessor implements CommandProcessor {
+  final HttpClient client = new HttpClient();
+
+  final Lock _lock = new Lock();
+
+  @override
+  Future<dynamic> post(Uri uri, dynamic params, {bool value: true}) async {
+    await _lock.acquire();
+    HttpClientRequest request = await client.postUrl(uri);
+    _setUpRequest(request);
+    request.headers.contentType = _contentTypeJson;
+    if (params != null) {
+      var body = UTF8.encode(JSON.encode(params));
+      request.contentLength = body.length;
+      request.add(body);
+    } else {
+      request.contentLength = 0;
+    }
+    return await _processResponse(await request.close(), value);
+  }
+
+  @override
+  Future<dynamic> get(Uri uri, {bool value: true}) async {
+    await _lock.acquire();
+    HttpClientRequest request = await client.getUrl(uri);
+    _setUpRequest(request);
+    return await _processResponse(await request.close(), value);
+  }
+
+  @override
+  Future<dynamic> delete(Uri uri, {bool value: true}) async {
+    await _lock.acquire();
+    HttpClientRequest request = await client.deleteUrl(uri);
+    _setUpRequest(request);
+    return await _processResponse(await request.close(), value);
+  }
+
+  @override
+  Future close() async {
+    await client.close(force: true);
+  }
+
+  _processResponse(HttpClientResponse response, bool value) async {
+    var respDecoded = await UTF8.decodeStream(response);
+    _lock.release();
+    Map respBody;
+    try {
+      respBody = JSON.decode(respDecoded);
+    } catch (e) {}
+
+    if (response.statusCode < 200 ||
+        response.statusCode > 299 ||
+        (respBody is Map &&
+            respBody['status'] != null &&
+            respBody['status'] != 0)) {
+      throw new WebDriverException(
+          httpStatusCode: response.statusCode,
+          httpReasonPhrase: response.reasonPhrase,
+          jsonResp: respBody);
+    }
+    if (value && respBody is Map) {
+      return respBody['value'];
+    }
+    return respBody;
+  }
+
+  void _setUpRequest(HttpClientRequest request) {
+    request.followRedirects = true;
+    request.headers.add(HttpHeaders.ACCEPT, "application/json");
+    request.headers.add(HttpHeaders.ACCEPT_CHARSET, UTF8.name);
+    request.headers.add(HttpHeaders.CACHE_CONTROL, "no-cache");
+  }
+}
diff --git a/lib/core.dart b/lib/core.dart
index cd6fd07..7369275 100644
--- a/lib/core.dart
+++ b/lib/core.dart
@@ -14,60 +14,6 @@
 
 library webdriver.core;
 
-import 'dart:async' show Future, Stream, StreamController;
-import 'dart:collection' show UnmodifiableMapView;
-import 'dart:convert' show BASE64;
-import 'dart:math' show Point, Rectangle;
-
-import 'package:stack_trace/stack_trace.dart' show Chain;
-
-import 'src/command_processor.dart' show CommandProcessor;
-import 'src/stepper.dart' show Stepper;
-
-export 'src/exception.dart';
-
-part 'src/alert.dart';
-part 'src/capabilities.dart';
-part 'src/command_event.dart';
-part 'src/common.dart';
-part 'src/keyboard.dart';
-part 'src/logs.dart';
-part 'src/mouse.dart';
-part 'src/navigation.dart';
-part 'src/options.dart';
-part 'src/target_locator.dart';
-part 'src/web_driver.dart';
-part 'src/web_element.dart';
-part 'src/window.dart';
-
-final Uri defaultUri = Uri.parse('http://127.0.0.1:4444/wd/hub/');
-
-Future<WebDriver> createDriver(CommandProcessor processor,
-    {Uri uri, Map<String, dynamic> desired}) async {
-  if (uri == null) {
-    uri = defaultUri;
-  }
-
-  if (desired == null) {
-    desired = Capabilities.empty;
-  }
-
-  Map response = await processor.post(
-      uri.resolve('session'), {'desiredCapabilities': desired},
-      value: false) as Map<String, dynamic>;
-  return new WebDriver(processor, uri, response['sessionId'],
-      new UnmodifiableMapView(response['value'] as Map<String, dynamic>));
-}
-
-Future<WebDriver> fromExistingSession(
-    CommandProcessor processor, String sessionId,
-    {Uri uri}) async {
-  if (uri == null) {
-    uri = defaultUri;
-  }
-
-  var response = await processor.get(uri.resolve('session/$sessionId'))
-      as Map<String, dynamic>;
-  return new WebDriver(
-      processor, uri, sessionId, new UnmodifiableMapView(response));
-}
+/// Consider this file as deprecated. This exists as an alias to async_core.dart
+/// for backward compatibility.
+export 'async_core.dart';
diff --git a/lib/html.dart b/lib/html.dart
index 104895d..e566184 100644
--- a/lib/html.dart
+++ b/lib/html.dart
@@ -18,13 +18,15 @@
 import 'dart:convert' show JSON;
 import 'dart:html' show HttpRequest, ProgressEvent;
 
-import 'package:webdriver/core.dart' as core
+import 'package:webdriver/async_core.dart' as core
     show createDriver, fromExistingSession, WebDriver;
-import 'package:webdriver/src/command_processor.dart' show CommandProcessor;
-import 'package:webdriver/src/exception.dart' show WebDriverException;
+import 'package:webdriver/src/async/command_processor.dart'
+    show CommandProcessor;
+import 'package:webdriver/src/async/exception.dart' show WebDriverException;
 import 'package:webdriver/support/async.dart' show Lock;
 
-export 'package:webdriver/core.dart' hide createDriver, fromExistingSession;
+export 'package:webdriver/async_core.dart'
+    hide createDriver, fromExistingSession;
 
 final Uri defaultUri = Uri.parse('http://127.0.0.1:4444/wd/hub/');
 
diff --git a/lib/io.dart b/lib/io.dart
index 332981c..35e615e 100644
--- a/lib/io.dart
+++ b/lib/io.dart
@@ -14,113 +14,6 @@
 
 library webdriver.io;
 
-import 'dart:async' show Future;
-import 'dart:convert' show JSON, UTF8;
-import 'dart:io'
-    show
-        ContentType,
-        HttpClient,
-        HttpClientRequest,
-        HttpClientResponse,
-        HttpHeaders;
-
-import 'package:webdriver/core.dart' as core
-    show createDriver, fromExistingSession, WebDriver;
-import 'package:webdriver/src/command_processor.dart' show CommandProcessor;
-import 'package:webdriver/src/exception.dart' show WebDriverException;
-import 'package:webdriver/support/async.dart' show Lock;
-
-export 'package:webdriver/core.dart' hide createDriver, fromExistingSession;
-
-/// Creates a WebDriver instance connected to the specified WebDriver server.
-///
-/// Note: WebDriver endpoints will be constructed using [resolve] against
-/// [uri]. Therefore, if [uri] does not end with a trailing slash, the
-/// last path component will be dropped.
-Future<core.WebDriver> createDriver({Uri uri, Map<String, dynamic> desired}) =>
-    core.createDriver(new IOCommandProcessor(), uri: uri, desired: desired);
-
-/// Creates a WebDriver instance connected to an existing session.
-///
-/// Note: WebDriver endpoints will be constructed using [resolve] against
-/// [uri]. Therefore, if [uri] does not end with a trailing slash, the
-/// last path component will be dropped.
-Future<core.WebDriver> fromExistingSession(String sessionId, {Uri uri}) =>
-    core.fromExistingSession(new IOCommandProcessor(), sessionId, uri: uri);
-
-final ContentType _contentTypeJson =
-    new ContentType("application", "json", charset: "utf-8");
-
-class IOCommandProcessor implements CommandProcessor {
-  final HttpClient client = new HttpClient();
-
-  final Lock _lock = new Lock();
-
-  @override
-  Future<dynamic> post(Uri uri, dynamic params, {bool value: true}) async {
-    await _lock.acquire();
-    HttpClientRequest request = await client.postUrl(uri);
-    _setUpRequest(request);
-    request.headers.contentType = _contentTypeJson;
-    if (params != null) {
-      var body = UTF8.encode(JSON.encode(params));
-      request.contentLength = body.length;
-      request.add(body);
-    } else {
-      request.contentLength = 0;
-    }
-    return await _processResponse(await request.close(), value);
-  }
-
-  @override
-  Future<dynamic> get(Uri uri, {bool value: true}) async {
-    await _lock.acquire();
-    HttpClientRequest request = await client.getUrl(uri);
-    _setUpRequest(request);
-    return await _processResponse(await request.close(), value);
-  }
-
-  @override
-  Future<dynamic> delete(Uri uri, {bool value: true}) async {
-    await _lock.acquire();
-    HttpClientRequest request = await client.deleteUrl(uri);
-    _setUpRequest(request);
-    return await _processResponse(await request.close(), value);
-  }
-
-  @override
-  Future close() async {
-    await client.close(force: true);
-  }
-
-  _processResponse(HttpClientResponse response, bool value) async {
-    var respDecoded = await UTF8.decodeStream(response);
-    _lock.release();
-    Map respBody;
-    try {
-      respBody = JSON.decode(respDecoded);
-    } catch (e) {}
-
-    if (response.statusCode < 200 ||
-        response.statusCode > 299 ||
-        (respBody is Map &&
-            respBody['status'] != null &&
-            respBody['status'] != 0)) {
-      throw new WebDriverException(
-          httpStatusCode: response.statusCode,
-          httpReasonPhrase: response.reasonPhrase,
-          jsonResp: respBody);
-    }
-    if (value && respBody is Map) {
-      return respBody['value'];
-    }
-    return respBody;
-  }
-
-  void _setUpRequest(HttpClientRequest request) {
-    request.followRedirects = true;
-    request.headers.add(HttpHeaders.ACCEPT, "application/json");
-    request.headers.add(HttpHeaders.ACCEPT_CHARSET, UTF8.name);
-    request.headers.add(HttpHeaders.CACHE_CONTROL, "no-cache");
-  }
-}
+/// Consider this file as deprecated. This exists as an alias to async_io.dart
+/// for backward compatibility.
+export 'package:webdriver/async_io.dart';
diff --git a/lib/src/alert.dart b/lib/src/async/alert.dart
similarity index 100%
rename from lib/src/alert.dart
rename to lib/src/async/alert.dart
diff --git a/lib/src/capabilities.dart b/lib/src/async/capabilities.dart
similarity index 100%
rename from lib/src/capabilities.dart
rename to lib/src/async/capabilities.dart
diff --git a/lib/src/command_event.dart b/lib/src/async/command_event.dart
similarity index 100%
rename from lib/src/command_event.dart
rename to lib/src/async/command_event.dart
diff --git a/lib/src/command_processor.dart b/lib/src/async/command_processor.dart
similarity index 100%
rename from lib/src/command_processor.dart
rename to lib/src/async/command_processor.dart
diff --git a/lib/src/common.dart b/lib/src/async/common.dart
similarity index 100%
rename from lib/src/common.dart
rename to lib/src/async/common.dart
diff --git a/lib/src/exception.dart b/lib/src/async/exception.dart
similarity index 100%
copy from lib/src/exception.dart
copy to lib/src/async/exception.dart
diff --git a/lib/src/keyboard.dart b/lib/src/async/keyboard.dart
similarity index 100%
rename from lib/src/keyboard.dart
rename to lib/src/async/keyboard.dart
diff --git a/lib/src/logs.dart b/lib/src/async/logs.dart
similarity index 100%
rename from lib/src/logs.dart
rename to lib/src/async/logs.dart
diff --git a/lib/src/mouse.dart b/lib/src/async/mouse.dart
similarity index 100%
rename from lib/src/mouse.dart
rename to lib/src/async/mouse.dart
diff --git a/lib/src/navigation.dart b/lib/src/async/navigation.dart
similarity index 100%
rename from lib/src/navigation.dart
rename to lib/src/async/navigation.dart
diff --git a/lib/src/options.dart b/lib/src/async/options.dart
similarity index 100%
rename from lib/src/options.dart
rename to lib/src/async/options.dart
diff --git a/lib/src/stepper.dart b/lib/src/async/stepper.dart
similarity index 100%
rename from lib/src/stepper.dart
rename to lib/src/async/stepper.dart
diff --git a/lib/src/target_locator.dart b/lib/src/async/target_locator.dart
similarity index 100%
rename from lib/src/target_locator.dart
rename to lib/src/async/target_locator.dart
diff --git a/lib/src/web_driver.dart b/lib/src/async/web_driver.dart
similarity index 100%
rename from lib/src/web_driver.dart
rename to lib/src/async/web_driver.dart
diff --git a/lib/src/web_element.dart b/lib/src/async/web_element.dart
similarity index 100%
rename from lib/src/web_element.dart
rename to lib/src/async/web_element.dart
diff --git a/lib/src/window.dart b/lib/src/async/window.dart
similarity index 100%
rename from lib/src/window.dart
rename to lib/src/async/window.dart
diff --git a/lib/src/sync/alert.dart b/lib/src/sync/alert.dart
new file mode 100644
index 0000000..945b565
--- /dev/null
+++ b/lib/src/sync/alert.dart
@@ -0,0 +1,53 @@
+// 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.
+
+import 'common.dart';
+import 'web_driver.dart';
+
+/// A JavaScript alert(), confirm(), or prompt() dialog
+class Alert {
+  /// The text of the JavaScript alert(), confirm(), or prompt() dialog.
+  final String text;
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  Alert(this.text, this._driver) : _resolver = new Resolver(_driver, '');
+
+  /// Accepts the currently displayed alert (may not be the alert for which this
+  /// object was created).
+  ///
+  ///  Throws [NoSuchAlertException] if there isn't currently an alert.
+  void accept() {
+    _resolver.post('accept_alert');
+  }
+
+  /// Dismisses the currently displayed alert (may not be the alert for which
+  /// this object was created).
+  ///
+  ///  Throws [NoSuchAlertException] if there isn't currently an alert.
+  void dismiss() {
+    _resolver.post('dismiss_alert');
+  }
+
+  /// Sends keys to the currently displayed alert (may not be the alert for
+  /// which this object was created).
+  ///
+  /// Throws [NoSuchAlertException] if there isn't currently an alert
+  void sendKeys(String keysToSend) {
+    _resolver.post('alert_text', {'text': keysToSend});
+  }
+
+  @override
+  String toString() => '$_driver.switchTo.alert[$text]';
+}
diff --git a/lib/src/sync/capabilities.dart b/lib/src/sync/capabilities.dart
new file mode 100644
index 0000000..43d9de6
--- /dev/null
+++ b/lib/src/sync/capabilities.dart
@@ -0,0 +1,71 @@
+// 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.
+
+/// Capabilities constants.
+class Capabilities {
+  static const String browserName = "browserName";
+  static const String platform = "platform";
+  static const String supportsJavascript = "javascriptEnabled";
+  static const String takesScreenshot = "takesScreenshot";
+  static const String version = "version";
+  static const String supportsAlerts = "handlesAlerts";
+  static const String supportSqlDatabase = "databaseEnabled";
+  static const String supportsLocationContext = "locationContextEnabled";
+  static const String supportsApplicationCache = "applicationCacheEnabled";
+  static const String supportsBrowserConnection = "browserConnectionEnabled";
+  static const String supportsFindingByCss = "cssSelectorsEnabled";
+  static const String proxy = "proxy";
+  static const String supportsWebStorage = "webStorageEnabled";
+  static const String rotatable = "rotatable";
+  static const String acceptSslCerts = "acceptSslCerts";
+  static const String hasNativeEvents = "nativeEvents";
+  static const String unexpectedAlertBehaviour = "unexpectedAlertBehaviour";
+  static const String loggingPrefs = "loggingPrefs";
+  static const String enableProfiling = "webdriver.logging.profiler.enabled";
+
+  static Map<String, dynamic> get chrome => empty
+    ..[browserName] = Browser.chrome
+    ..[version] = ''
+    ..[platform] = BrowserPlatform.any;
+
+  static Map<String, dynamic> get firefox => empty
+    ..[browserName] = Browser.firefox
+    ..[version] = ''
+    ..[platform] = BrowserPlatform.any;
+
+  static Map<String, dynamic> get android => empty
+    ..[browserName] = Browser.android
+    ..[version] = ''
+    ..[platform] = BrowserPlatform.android;
+
+  static Map<String, dynamic> get empty => new Map<String, dynamic>();
+}
+
+/// Browser name constants.
+class Browser {
+  static const String firefox = "firefox";
+  static const String safari = "safari";
+  static const String opera = "opera";
+  static const String chrome = "chrome";
+  static const String android = "android";
+  static const String ie = "internet explorer";
+  static const String iphone = "iPhone";
+  static const String ipad = "iPad";
+}
+
+/// Browser operating system constants.
+class BrowserPlatform {
+  static const String any = "ANY";
+  static const String android = "ANDROID";
+}
diff --git a/lib/src/sync/command_event.dart b/lib/src/sync/command_event.dart
new file mode 100644
index 0000000..ce9725b
--- /dev/null
+++ b/lib/src/sync/command_event.dart
@@ -0,0 +1,36 @@
+// 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.
+class WebDriverCommandEvent {
+  final String method;
+  final String endPoint;
+  final params;
+  final StackTrace stackTrace;
+  final DateTime startTime;
+  final DateTime endTime;
+  final exception;
+  final result;
+
+  WebDriverCommandEvent(
+      {this.method,
+      this.endPoint,
+      this.params,
+      this.startTime,
+      this.endTime,
+      this.exception,
+      this.result,
+      this.stackTrace});
+
+  String toString() => '[$startTime - $endTime] $method $endPoint($params) => '
+      '${exception != null ? exception : result}';
+}
diff --git a/test/config.dart b/lib/src/sync/command_processor.dart
similarity index 63%
rename from test/config.dart
rename to lib/src/sync/command_processor.dart
index ceeb8be..445555d 100644
--- a/test/config.dart
+++ b/lib/src/sync/command_processor.dart
@@ -12,17 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-library config;
+library webdriver.command_processor;
 
-import 'dart:async' show Future;
+/// Interface for synchronous HTTP access.
+abstract class CommandProcessor {
+  Object post(Uri uri, dynamic params, {bool value: true});
 
-import 'package:webdriver/core.dart' show WebDriver;
+  Object get(Uri uri, {bool value: true});
 
-Future<WebDriver> createTestDriver(
-    {Map<String, dynamic> additionalCapabilities}) {
-  throw new UnimplementedError("createTestDriver is abstract");
-}
+  Object delete(Uri uri, {bool value: true});
 
-String get testPagePath {
-  throw new UnimplementedError("createTestDriver is abstract");
+  void close();
 }
diff --git a/lib/src/sync/common.dart b/lib/src/sync/common.dart
new file mode 100644
index 0000000..fd82217
--- /dev/null
+++ b/lib/src/sync/common.dart
@@ -0,0 +1,133 @@
+// 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.
+
+import 'web_driver.dart';
+import 'web_element.dart';
+
+const String elementStr = 'ELEMENT';
+
+/// Simple class to provide access to indexed properties such as WebElement
+/// attributes or css styles.
+class Attributes  {
+  final Resolver _resolver;
+  Attributes(driver, command) : _resolver = new Resolver(driver, command);
+
+  String operator [](String name) => _resolver.get(name) as String;
+}
+
+abstract class SearchContext {
+  WebDriver get driver;
+
+  /// Searches for multiple elements within the context.
+  List<WebElement> findElements(By by);
+
+  /// Searches for an element within the context.
+  ///
+  /// Throws [NoSuchElementException] if no matching element is found.
+  WebElement findElement(By by);
+}
+
+class Resolver {
+  final String prefix;
+  final WebDriver driver;
+
+  Resolver(this.driver, this.prefix);
+
+  dynamic post(String command, [param]) =>
+      driver.postRequest(_resolve(command), param);
+
+  dynamic get(String command) => driver.getRequest(_resolve(command));
+
+  dynamic delete(String command) => driver.deleteRequest(_resolve(command));
+
+  String _resolve(command) {
+    if (prefix == null || prefix.isEmpty) {
+      return command;
+    }
+    if (command == null || command.isEmpty) {
+      return prefix;
+    }
+    return '$prefix/$command';
+  }
+}
+
+class By {
+  final String _using;
+  final String _value;
+
+  const By(this._using, this._value);
+
+  /// Returns an element whose ID attribute matches the search value.
+  const By.id(String id) : this('id', id);
+
+  /// Returns an element matching an XPath expression.
+  const By.xpath(String xpath) : this('xpath', xpath);
+
+  /// Returns an anchor element whose visible text matches the search value.
+  const By.linkText(String linkText) : this('link text', linkText);
+
+  /// Returns an anchor element whose visible text partially matches the search
+  /// value.
+  const By.partialLinkText(String partialLinkText)
+      : this('partial link text', partialLinkText);
+
+  /// Returns an element whose NAME attribute matches the search value.
+  const By.name(String name) : this('name', name);
+
+  /// Returns an element whose tag name matches the search value.
+  const By.tagName(String tagName) : this('tag name', tagName);
+
+  /**
+   * Returns an element whose class name contains the search value; compound
+   * class names are not permitted
+   */
+  const By.className(String className) : this('class name', className);
+
+  /// Returns an element matching a CSS selector.
+  const By.cssSelector(String cssSelector) : this('css selector', cssSelector);
+
+  Map<String, String> toJson() => {'using': _using, 'value': _value};
+
+  @override
+  String toString() {
+    var constructor;
+    switch (_using) {
+      case 'link text':
+        constructor = 'linkText';
+        break;
+      case 'partial link text':
+        constructor = 'partialLinkText';
+        break;
+      case 'tag name':
+        constructor = 'tagName';
+        break;
+      case 'class name':
+        constructor = 'className';
+        break;
+      case 'css selector':
+        constructor = 'cssSelector';
+        break;
+      default:
+        constructor = _using;
+    }
+    return 'By.$constructor($_value)';
+  }
+
+  @override
+  int get hashCode => _using.hashCode * 3 + _value.hashCode;
+
+  @override
+  bool operator ==(other) =>
+      other is By && other._using == this._using && other._value == this._value;
+}
diff --git a/lib/src/exception.dart b/lib/src/sync/exception.dart
similarity index 100%
rename from lib/src/exception.dart
rename to lib/src/sync/exception.dart
diff --git a/lib/src/sync/keyboard.dart b/lib/src/sync/keyboard.dart
new file mode 100644
index 0000000..ce411a1
--- /dev/null
+++ b/lib/src/sync/keyboard.dart
@@ -0,0 +1,112 @@
+// 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.
+
+import 'common.dart';
+import 'web_driver.dart';
+
+class Keyboard {
+  static const String nullChar = '\uE000';
+  static const String cancel = '\uE001';
+  static const String help = '\uE002';
+  static const String backSpace = '\uE003';
+  static const String tab = '\uE004';
+  static const String clear = '\uE005';
+  static const String returnChar = '\uE006';
+  static const String enter = '\uE007';
+  static const String shift = '\uE008';
+  static const String control = '\uE009';
+  static const String alt = '\uE00A';
+  static const String pause = '\uE00B';
+  static const String escape = '\uE00C';
+  static const String space = '\uE00D';
+  static const String pageUp = '\uE00E';
+  static const String pageDown = '\uE00F';
+  static const String end = '\uE010';
+  static const String home = '\uE011';
+  static const String left = '\uE012';
+  static const String up = '\uE013';
+  static const String right = '\uE014';
+  static const String down = '\uE015';
+  static const String insert = '\uE016';
+  static const String deleteChar = '\uE017';
+  static const String semicolon = '\uE018';
+  static const String equals = '\uE019';
+  static const String numpad0 = '\uE01A';
+  static const String numpad1 = '\uE01B';
+  static const String numpad2 = '\uE01C';
+  static const String numpad3 = '\uE01D';
+  static const String numpad4 = '\uE01E';
+  static const String numpad5 = '\uE01F';
+  static const String numpad6 = '\uE020';
+  static const String numpad7 = '\uE021';
+  static const String numpad8 = '\uE022';
+  static const String numpad9 = '\uE023';
+  static const String multiply = '\uE024';
+  static const String add = '\uE025';
+  static const String separator = '\uE026';
+  static const String subtract = '\uE027';
+  static const String decimal = '\uE028';
+  static const String divide = '\uE029';
+  static const String f1 = '\uE031';
+  static const String f2 = '\uE032';
+  static const String f3 = '\uE033';
+  static const String f4 = '\uE034';
+  static const String f5 = '\uE035';
+  static const String f6 = '\uE036';
+  static const String f7 = '\uE037';
+  static const String f8 = '\uE038';
+  static const String f9 = '\uE039';
+  static const String f10 = '\uE03A';
+  static const String f11 = '\uE03B';
+  static const String f12 = '\uE03C';
+  static const String command = '\uE03D';
+  static const String meta = command;
+
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  Keyboard(this._driver) : _resolver = new Resolver(_driver, '');
+
+  /// Simulate pressing many keys at once as a 'chord'.
+  void sendChord(Iterable<String> chordToSend) {
+    sendKeys(createChord(chordToSend));
+  }
+
+  /// Creates a string representation of a chord suitable for use in WebDriver.
+  String createChord(Iterable<String> chord) {
+    StringBuffer chordString = new StringBuffer();
+    for (String s in chord) {
+      chordString.write(s);
+    }
+    chordString.write(nullChar);
+
+    return chordString.toString();
+  }
+
+  /// Send [keysToSend] to the active element.
+  void sendKeys(String keysToSend) {
+    _resolver.post('keys', {
+      'value': [keysToSend]
+    });
+  }
+
+  @override
+  String toString() => '$_driver.keyboard';
+
+  @override
+  int get hashCode => _driver.hashCode;
+
+  @override
+  bool operator ==(other) => other is Keyboard && other._driver == _driver;
+}
diff --git a/lib/src/sync/logs.dart b/lib/src/sync/logs.dart
new file mode 100644
index 0000000..74ebb85
--- /dev/null
+++ b/lib/src/sync/logs.dart
@@ -0,0 +1,73 @@
+// 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.
+
+import 'common.dart';
+import 'web_driver.dart';
+
+class Logs {
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  Logs(this._driver) : _resolver = new Resolver(_driver, 'log');
+
+  List<LogEntry> get(String logType) {
+    var entries = _resolver.post('', {'type': logType}) as List<Map>;
+    return entries.map((e) => new LogEntry.fromMap(e)).toList();
+  }
+
+  @override
+  String toString() => '$_driver.logs';
+
+  @override
+  int get hashCode => _driver.hashCode;
+
+  @override
+  bool operator ==(other) => other is Logs && other._driver == _driver;
+}
+
+class LogEntry {
+  final String message;
+  final DateTime timestamp;
+  final String level;
+
+  const LogEntry(this.message, this.timestamp, this.level);
+
+  LogEntry.fromMap(Map map)
+      : this(
+            map['message'],
+            new DateTime.fromMillisecondsSinceEpoch(map['timestamp'].toInt(),
+                isUtc: true),
+            map['level']);
+
+  @override
+  String toString() => '$level[$timestamp]: $message';
+}
+
+class LogType {
+  static const String browser = 'browser';
+  static const String client = 'client';
+  static const String driver = 'driver';
+  static const String performance = 'performance';
+  static const String profiler = 'profiler';
+  static const String server = 'server';
+}
+
+class LogLevel {
+  static const String off = 'OFF';
+  static const String severe = 'SEVERE';
+  static const String warning = 'WARNING';
+  static const String info = 'INFO';
+  static const String debug = 'DEBUG';
+  static const String all = 'ALL';
+}
diff --git a/lib/src/sync/mouse.dart b/lib/src/sync/mouse.dart
new file mode 100644
index 0000000..321336e
--- /dev/null
+++ b/lib/src/sync/mouse.dart
@@ -0,0 +1,110 @@
+// 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.
+
+import 'common.dart';
+import 'web_driver.dart';
+import 'web_element.dart';
+
+class MouseButton {
+  /// The primary button is usually the left button or the only button on
+  /// single-button devices, used to activate a user interface control or select
+  /// text.
+  static const MouseButton primary = const MouseButton(0);
+
+  /// The auxiliary button is usually the middle button, often combined with a
+  /// mouse wheel.
+  static const MouseButton auxiliary = const MouseButton(1);
+
+  /// The secondary button is usually the right button, often used to display a
+  /// context menu.
+  static const MouseButton secondary = const MouseButton(2);
+
+  final int value;
+
+  /// [value] for a mouse button is defined in
+  /// https://w3c.github.io/uievents/#widl-MouseEvent-button
+  const MouseButton(this.value);
+}
+
+class Mouse {
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  Mouse(this._driver) : _resolver = new Resolver(_driver, '');
+
+  /// Click any mouse button (at the coordinates set by the last moveTo).
+  void click([MouseButton button]) {
+    var json = {};
+    if (button is MouseButton) {
+      json['button'] = button.value;
+    }
+    _resolver.post('click', json);
+  }
+
+  /// Click and hold any mouse button (at the coordinates set by the last
+  /// moveTo command).
+  void down([MouseButton button]) {
+    var json = {};
+    if (button is MouseButton) {
+      json['button'] = button.value;
+    }
+    _resolver.post('buttondown', json);
+  }
+
+  /// Releases the mouse button previously held (where the mouse is currently at).
+  void up([MouseButton button]) {
+    var json = {};
+    if (button is MouseButton) {
+      json['button'] = button.value;
+    }
+    _resolver.post('buttonup', json);
+  }
+
+  /// Double-clicks at the current mouse coordinates (set by moveTo).
+  void doubleClick() {
+    _resolver.post('doubleclick');
+  }
+
+  /// Move the mouse.
+  ///
+  /// If [element] is specified and [xOffset] and [yOffset] are not, will move
+  /// the mouse to the center of the [element].
+  ///
+  /// If [xOffset] and [yOffset] are specified, will move the mouse that distance
+  /// from its current location.
+  ///
+  /// If all three are specified, will move the mouse to the offset relative to
+  /// the top-left corner of the [element].
+  /// All other combinations of parameters are illegal.
+  void moveTo({WebElement element, int xOffset, int yOffset}) {
+    var json = {};
+    if (element is WebElement) {
+      json['element'] = element.id;
+    }
+    if (xOffset is num && yOffset is num) {
+      json['xoffset'] = xOffset.floor();
+      json['yoffset'] = yOffset.floor();
+    }
+    _resolver.post('moveto', json);
+  }
+
+  @override
+  String toString() => '$_driver.mouse';
+
+  @override
+  int get hashCode => _driver.hashCode;
+
+  @override
+  bool operator ==(other) => other is Mouse && other._driver == _driver;
+}
diff --git a/lib/src/sync/navigation.dart b/lib/src/sync/navigation.dart
new file mode 100644
index 0000000..4b29cb1
--- /dev/null
+++ b/lib/src/sync/navigation.dart
@@ -0,0 +1,47 @@
+// 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.
+
+import 'common.dart';
+import 'web_driver.dart';
+
+class Navigation {
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  Navigation(this._driver) : _resolver = new Resolver(_driver, '');
+
+  ///  Navigate forwards in the browser history, if possible.
+  void forward() {
+    _resolver.post('forward');
+  }
+
+  /// Navigate backwards in the browser history, if possible.
+  void back() {
+    _resolver.post('back');
+  }
+
+  /// Refresh the current page.
+  void refresh() {
+    _resolver.post('refresh');
+  }
+
+  @override
+  String toString() => '$_driver.navigate';
+
+  @override
+  int get hashCode => _driver.hashCode;
+
+  @override
+  bool operator ==(other) => other is Navigation && other._driver == _driver;
+}
diff --git a/lib/src/sync/options.dart b/lib/src/sync/options.dart
new file mode 100644
index 0000000..0591f11
--- /dev/null
+++ b/lib/src/sync/options.dart
@@ -0,0 +1,139 @@
+// 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.
+
+import 'common.dart';
+import 'web_driver.dart';
+
+class Cookies {
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  Cookies(this._driver) : _resolver = new Resolver(_driver, 'cookie');
+
+  /// Set a cookie.
+  void add(Cookie cookie) {
+    _resolver.post('', {'cookie': cookie});
+  }
+
+  /// Delete the cookie with the given [name].
+  void delete(String name) {
+    _resolver.delete('$name');
+  }
+
+  /// Delete all cookies visible to the current page.
+  void deleteAll() {
+    _resolver.delete('');
+  }
+
+  /// Retrieve all cookies visible to the current page.
+  List<Cookie> get all {
+    var cookies = _resolver.get('') as List<Map<String, dynamic>>;
+    return cookies.map((c) => new Cookie.fromJson(c)).toList();
+  }
+
+  @override
+  String toString() => '$_driver.cookies';
+
+  @override
+  int get hashCode => _driver.hashCode;
+
+  @override
+  bool operator ==(other) => other is Cookies && other._driver == _driver;
+}
+
+class Cookie {
+  /// The name of the cookie.
+  final String name;
+
+  /// The cookie value.
+  final String value;
+
+  /// (Optional) The cookie path.
+  final String path;
+
+  /// (Optional) The domain the cookie is visible to.
+  final String domain;
+
+  /// (Optional) Whether the cookie is a secure cookie.
+  final bool secure;
+
+  /// (Optional) When the cookie expires.
+  final DateTime expiry;
+
+  Cookie(this.name, this.value,
+      {this.path, this.domain, this.secure, this.expiry});
+
+  factory Cookie.fromJson(Map<String, dynamic> json) {
+    var expiry;
+    if (json['expiry'] is num) {
+      expiry = new DateTime.fromMillisecondsSinceEpoch(
+          json['expiry'].toInt() * 1000,
+          isUtc: true);
+    }
+    return new Cookie(json['name'], json['value'],
+        path: json['path'],
+        domain: json['domain'],
+        secure: json['secure'],
+        expiry: expiry);
+  }
+
+  Map<String, dynamic> toJson() {
+    var json = <String, dynamic>{'name': name, 'value': value};
+    if (path is String) {
+      json['path'] = path;
+    }
+    if (domain is String) {
+      json['domain'] = domain;
+    }
+    if (secure is bool) {
+      json['secure'] = secure;
+    }
+    if (expiry is DateTime) {
+      json['expiry'] = (expiry.millisecondsSinceEpoch / 1000).ceil();
+    }
+    return json;
+  }
+
+  @override
+  String toString() => 'Cookie${toJson()}';
+}
+
+class Timeouts {
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  Timeouts(this._driver) : _resolver = new Resolver(_driver, 'timeouts');
+
+  void _set(String type, Duration duration) {
+    _resolver.post('', {'type': type, 'ms': duration.inMilliseconds});
+  }
+
+  /// Set the script timeout.
+  void setScriptTimeout(Duration duration) => _set('script', duration);
+
+  /// Set the implicit timeout.
+  void setImplicitTimeout(Duration duration) => _set('implicit', duration);
+
+  /// Set the page load timeout.
+  void setPageLoadTimeout(Duration duration) => _set('page load', duration);
+
+  @override
+  String toString() => '$_driver.timeouts';
+
+  @override
+  int get hashCode => _driver.hashCode;
+
+  @override
+  bool operator ==(other) => other is Timeouts && other._driver == _driver;
+}
diff --git a/lib/src/sync/target_locator.dart b/lib/src/sync/target_locator.dart
new file mode 100644
index 0000000..560b652
--- /dev/null
+++ b/lib/src/sync/target_locator.dart
@@ -0,0 +1,71 @@
+// 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.
+
+import 'alert.dart';
+import 'common.dart';
+import 'web_driver.dart';
+import 'window.dart';
+
+class TargetLocator {
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  TargetLocator(this._driver) : _resolver = new Resolver(_driver, '');
+
+  /// Change focus to another frame on the page.
+  /// If [frame] is a:
+  ///   [int]: select by its zero-based index
+  ///   [String]: select frame by the name of the frame window or the id of the
+  ///           frame or iframe tag.
+  ///   [WebElement]: select the frame for a previously found frame or iframe
+  ///               element.
+  ///   not provided: selects the first frame on the page or the main document.
+  ///
+  ///   Throws [NoSuchFrameException] if the specified frame can't be found.
+  void frame([frame]) {
+    _resolver.post('frame', {'id': frame});
+  }
+
+  /// Switch the focus of void commands for this driver to the window with the
+  /// given name/handle.
+  ///
+  /// Throws [NoSuchWindowException] if the specified window can't be found.
+  void window(dynamic window) {
+    if (window is Window) {
+      _resolver.post('window', {'name': window.handle});
+    } else if (window is String) {
+      _resolver.post('window', {'name': window});
+    } else {
+      throw 'Unsupported type: ${window.runtimeType}';
+    }
+  }
+
+  /// Switches to the currently active modal dialog for this particular driver
+  /// instance.
+  ///
+  /// Throws [NoSuchAlertException] if there is not currently an alert.
+  Alert get alert {
+    var text = _resolver.get('alert_text');
+    return new Alert(text, _driver);
+  }
+
+  @override
+  String toString() => '$_driver.switchTo';
+
+  @override
+  int get hashCode => _driver.hashCode;
+
+  @override
+  bool operator ==(other) => other is TargetLocator && other._driver == _driver;
+}
diff --git a/lib/src/sync/web_driver.dart b/lib/src/sync/web_driver.dart
new file mode 100644
index 0000000..e121013
--- /dev/null
+++ b/lib/src/sync/web_driver.dart
@@ -0,0 +1,282 @@
+// 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.
+
+import 'dart:convert' show BASE64;
+import 'package:stack_trace/stack_trace.dart' show Chain;
+
+import 'command_event.dart';
+import 'command_processor.dart';
+import 'common.dart';
+import 'exception.dart';
+import 'keyboard.dart';
+import 'logs.dart';
+import 'mouse.dart';
+import 'navigation.dart';
+import 'options.dart';
+import 'target_locator.dart';
+import 'window.dart';
+import 'web_element.dart';
+
+typedef void 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;
+
+  /// If true, WebDriver actions are recorded as [WebDriverCommandEvent]s.
+  bool notifyListeners = true;
+
+  final _commandListeners = new List<WebDriverListener>();
+
+  WebDriver(this._commandProcessor, Uri uri, String id, this.capabilities,
+      {this.filterStackTraces: true})
+      : this.uri = uri,
+        this.id = id,
+        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.
+  String get currentUrl => getRequest('url') as String;
+
+  /// navigate to the specified url
+  void get(/* Uri | String */ url) {
+    if (url is Uri) {
+      url = url.toString();
+    }
+    postRequest('url', {'url': url as String});
+  }
+
+  /// The title of the current page.
+  String get title => getRequest('title') as String;
+
+  /// Search for multiple elements within the entire current page.
+  @override
+  List<WebElement> findElements(By by) {
+    var elements = postRequest('elements', by);
+    int i = 0;
+
+    final webElements = new List<WebElement>();
+    for (var element in elements) {
+      webElements.add(new WebElement(this, element[elementStr], this, by, i++));
+    }
+    return webElements;
+  }
+
+  /// Search for an element within the entire current page.
+  /// Throws [NoSuchElementException] if a matching element is not found.
+  @override
+  WebElement findElement(By by) {
+    var element = postRequest('element', by);
+    return new WebElement(this, element[elementStr], this, by);
+  }
+
+  /// An artist's rendition of the current page's source.
+  String get pageSource => getRequest('source') as String;
+
+  /// Close the current window, quitting the browser if it is the last window.
+  void close() {
+    deleteRequest('window');
+  }
+
+  /// Quit the browser.
+  void quit({bool closeSession: true}) {
+    try {
+      if (closeSession) {
+        _commandProcessor.delete(uri.resolve('session/$id'));
+      }
+    } finally {
+      _commandProcessor.close();
+    }
+  }
+
+  /// Handles for all of the currently displayed tabs/windows.
+  List<Window> get windows {
+    var handles = getRequest('window_handles') as List<String>;
+    return handles.map((h) => new Window(this, h)).toList();
+  }
+
+  /// Handle for the active tab/window.
+  Window get window {
+    var handle = getRequest('window_handle');
+    return new Window(this, handle);
+  }
+
+  /// The currently focused element, or the body element if no element has
+  /// focus.
+  WebElement get activeElement {
+    var element = postRequest('element/active');
+    if (element != null) {
+      return new WebElement(this, element[elementStr], 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.
+  String captureScreenshotAsBase64() => getRequest('screenshot');
+
+  /// Take a screenshot of the current page as PNG as list of uint8.
+  List<int> captureScreenshotAsList() {
+    var base64Encoded = captureScreenshotAsBase64();
+    return BASE64.decode(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.
+  dynamic executeAsync(String script, List args) => _recursiveElementify(
+      postRequest('execute_async', {'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
+  /// 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.
+  dynamic execute(String script, List args) => _recursiveElementify(
+      postRequest('execute', {'script': script, 'args': args}));
+
+  dynamic _recursiveElementify(result) {
+    if (result is Map) {
+      if (result.length == 1 && result.containsKey(elementStr)) {
+        return new WebElement(this, result[elementStr], this, 'javascript');
+      } else {
+        var newResult = {};
+        result.forEach((key, value) {
+          newResult[key] = _recursiveElementify(value);
+        });
+        return newResult;
+      }
+    } else if (result is List) {
+      return result.map((value) => _recursiveElementify(value)).toList();
+    } else {
+      return result;
+    }
+  }
+
+  dynamic postRequest(String command, [params]) => _performRequestWithLog(
+      () => _commandProcessor.post(_resolve(command), params),
+      'POST',
+      command,
+      params);
+
+  dynamic getRequest(String command) => _performRequestWithLog(
+      () => _commandProcessor.get(_resolve(command)), 'GET', command, null);
+
+  dynamic deleteRequest(String command) => _performRequestWithLog(
+      () => _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.
+  dynamic _performRequestWithLog(
+      Function fn, String method, String command, params) {
+    return _performRequest(fn, method, command, params);
+  }
+
+  // Performs the request. This will not notify any listeners or
+  // onCommandController. This should only be called from
+  // _performRequestWithLog.
+  dynamic _performRequest(Function fn, String method, String command, params) {
+    var startTime = new DateTime.now();
+    var trace = new Chain.current();
+    if (filterStackTraces) {
+      trace = trace.foldFrames(
+          (f) => f.library.startsWith('package:webdriver/'),
+          terse: true);
+    }
+    var result;
+    var exception;
+    try {
+      result = fn();
+      return result;
+    } catch (e) {
+      exception = e;
+      rethrow;
+    } finally {
+      if (notifyListeners) {
+        for (WebDriverListener listener in _commandListeners) {
+          listener(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)';
+}
diff --git a/lib/src/sync/web_element.dart b/lib/src/sync/web_element.dart
new file mode 100644
index 0000000..2aa26ad
--- /dev/null
+++ b/lib/src/sync/web_element.dart
@@ -0,0 +1,155 @@
+// 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.
+
+import 'dart:math' show Point, Rectangle;
+
+import 'common.dart';
+import 'web_driver.dart';
+
+class WebElement implements SearchContext {
+  final String _elementPrefix;
+  final Resolver _resolver;
+
+  final String id;
+
+  /// The context from which this element was found.
+  final SearchContext context;
+
+  final WebDriver driver;
+
+  /// How the element was located from the context.
+  final dynamic /* String | Finder */ locator;
+
+  /// The index of this element in the set of element founds. If the method
+  /// used to find this element always returns one element, then this is null.
+  final int index;
+
+  WebElement(this.driver, id, [this.context, this.locator, this.index])
+      : this.id = id,
+        _elementPrefix = 'element/$id',
+        _resolver = new Resolver(driver, 'element/$id');
+
+  /// Click on this element.
+  void click() {
+    _resolver.post('click');
+  }
+
+  /// Submit this element if it is part of a form.
+  void submit() {
+    _resolver.post('submit');
+  }
+
+  /// Send [keysToSend] to this element.
+  void sendKeys(String keysToSend) {
+    _resolver.post('value', {
+      'value': [keysToSend]
+    });
+  }
+
+  /// Clear the content of a text element.
+  void clear() {
+    _resolver.post('clear');
+  }
+
+  /// Is this radio button/checkbox selected?
+  bool get selected => _resolver.get('selected') as bool;
+
+  /// Is this form element enabled?
+  bool get enabled => _resolver.get('enabled') as bool;
+
+  /// Is this element visible in the page?
+  bool get displayed => _resolver.get('displayed') as bool;
+
+  /// The location within the document of this element.
+  Point get location {
+    var point = _resolver.get('location');
+    return new Point<int>(point['x'].toInt(), point['y'].toInt());
+  }
+
+  /// The size of this element.
+  Rectangle<int> get size {
+    var size = _resolver.get('size');
+    return new Rectangle<int>(
+        0, 0, size['width'].toInt(), size['height'].toInt());
+  }
+
+  /// The tag name for this element.
+  String get name => _resolver.get('name') as String;
+
+  ///  Visible text within this element.
+  String get text => _resolver.get('text') as String;
+
+  ///Find an element nested within this element.
+  ///
+  /// Throws [NoSuchElementException] if matching element is not found.
+  WebElement findElement(By by) {
+    var element = _resolver.post('element', by);
+    return new WebElement(driver, element[elementStr], this, by);
+  }
+
+  /// Find multiple elements nested within this element.
+  List<WebElement> findElements(By by) {
+    var elements = _resolver.post('elements', by) as Iterable;
+    int i = 0;
+    final webElements = new List<WebElement>();
+    for (var element in elements) {
+      webElements
+          .add(new WebElement(driver, element[elementStr], this, by, i++));
+    }
+    return webElements;
+  }
+
+  /// Access to the HTML attributes of this tag.
+  ///
+  /// TODO(DrMarcII): consider special handling of boolean attributes.
+  Attributes get attributes => new Attributes(driver, '$_elementPrefix/attribute');
+
+  /// Access to the cssProperties of this element.
+  ///
+  /// TODO(DrMarcII): consider special handling of color and possibly other
+  /// properties.
+  Attributes get cssProperties => new Attributes(driver, '$_elementPrefix/css');
+
+  /// Does this element represent the same element as another element?
+  /// Not the same as ==
+  bool equals(WebElement other) => _resolver.get('equals/${other.id}') as bool;
+
+  Map<String, String> toJson() => {elementStr: id};
+
+  @override
+  int get hashCode => driver.hashCode * 3 + id.hashCode;
+
+  @override
+  bool operator ==(other) =>
+      other is WebElement && other.driver == this.driver && other.id == this.id;
+
+  @override
+  String toString() {
+    var out = new StringBuffer()..write(context);
+    if (locator is By) {
+      if (index == null) {
+        out..write('.findElement(');
+      } else {
+        out..write('.findElements(');
+      }
+      out..write(locator)..write(')');
+    } else {
+      out..write('.')..write(locator);
+    }
+    if (index != null) {
+      out..write('[')..write(index)..write(']');
+    }
+    return out.toString();
+  }
+}
diff --git a/lib/src/sync/window.dart b/lib/src/sync/window.dart
new file mode 100644
index 0000000..fd3ff76
--- /dev/null
+++ b/lib/src/sync/window.dart
@@ -0,0 +1,68 @@
+// 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.
+
+import 'dart:math' show Point, Rectangle;
+
+import 'common.dart';
+import 'web_driver.dart';
+
+class Window {
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  final String handle;
+
+  Window(this._driver, this.handle) :
+        _resolver = new Resolver(_driver, 'window/$handle');
+
+  /// The size of this window.
+  Rectangle<int> get size {
+    var size = _resolver.get('size');
+    return new Rectangle<int>(
+        0, 0, size['width'].toInt(), size['height'].toInt());
+  }
+
+  /// The location of this window.
+  Point<int> get location {
+    var point = _resolver.get('position');
+    return new Point<int>(point['x'].toInt(), point['y'].toInt());
+  }
+
+  /// Maximize this window.
+  void maximize() {
+    _resolver.post('maximize');
+  }
+
+  /// Set this window size.
+  void setSize(Rectangle<int> size) {
+    _resolver.post('size', {'width': size.width.toInt(), 'height': size.height.toInt()});
+  }
+
+  /// Set this window location.
+  void setLocation(Point<int> point) {
+    _resolver.post('position', {'x': point.x.toInt(), 'y': point.y.toInt()});
+  }
+
+  @override
+  int get hashCode => handle.hashCode * 3 + _driver.hashCode;
+
+  @override
+  bool operator ==(other) =>
+      other is Window &&
+      other._driver == this._driver &&
+      other.handle == this.handle;
+
+  @override
+  String toString() => '$_driver.windows[$handle]';
+}
diff --git a/lib/support/forwarder.dart b/lib/support/forwarder.dart
index 8d13c44..bec1eee 100644
--- a/lib/support/forwarder.dart
+++ b/lib/support/forwarder.dart
@@ -19,7 +19,8 @@
 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;
+import 'package:webdriver/async_core.dart'
+    show By, WebDriver, WebDriverException;
 
 final _contentTypeJson =
     new ContentType('application', 'json', charset: 'utf-8');
diff --git a/lib/support/stdio_stepper.dart b/lib/support/stdio_stepper.dart
index cf44305..27cde5a 100644
--- a/lib/support/stdio_stepper.dart
+++ b/lib/support/stdio_stepper.dart
@@ -18,7 +18,7 @@
 import 'dart:convert' show Encoding, JSON;
 import 'dart:io' show exit, Stdin, stdin, SYSTEM_ENCODING;
 
-import 'package:webdriver/src/stepper.dart';
+import 'package:webdriver/src/async/stepper.dart';
 
 LineReader _stdinLineReader;
 
diff --git a/lib/sync_core.dart b/lib/sync_core.dart
new file mode 100644
index 0000000..e94326c
--- /dev/null
+++ b/lib/sync_core.dart
@@ -0,0 +1,69 @@
+// 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.sync_core;
+
+import 'dart:collection' show UnmodifiableMapView;
+
+import 'package:webdriver/src/sync/capabilities.dart' show Capabilities;
+import 'package:webdriver/src/sync/command_processor.dart'
+    show CommandProcessor;
+import 'package:webdriver/src/sync/web_driver.dart' show WebDriver;
+
+export 'package:webdriver/src/sync/alert.dart';
+export 'package:webdriver/src/sync/capabilities.dart';
+export 'package:webdriver/src/sync/command_event.dart';
+export 'package:webdriver/src/sync/command_processor.dart';
+export 'package:webdriver/src/sync/common.dart';
+export 'package:webdriver/src/sync/exception.dart';
+export 'package:webdriver/src/sync/keyboard.dart';
+export 'package:webdriver/src/sync/logs.dart';
+export 'package:webdriver/src/sync/mouse.dart';
+export 'package:webdriver/src/sync/navigation.dart';
+export 'package:webdriver/src/sync/options.dart';
+export 'package:webdriver/src/sync/target_locator.dart';
+export 'package:webdriver/src/sync/web_driver.dart';
+export 'package:webdriver/src/sync/web_element.dart';
+export 'package:webdriver/src/sync/window.dart';
+
+final Uri defaultUri = Uri.parse('http://127.0.0.1:4444/wd/hub/');
+
+WebDriver createDriver(CommandProcessor processor,
+    {Uri uri, Map<String, dynamic> desired}) {
+  if (uri == null) {
+    uri = defaultUri;
+  }
+
+  if (desired == null) {
+    desired = Capabilities.empty;
+  }
+
+  Map response = processor.post(
+      uri.resolve('session'), {'desiredCapabilities': desired},
+      value: false) as Map<String, dynamic>;
+  return new WebDriver(processor, uri, response['sessionId'],
+      new UnmodifiableMapView(response['value'] as Map<String, dynamic>));
+}
+
+WebDriver fromExistingSession(CommandProcessor processor, String sessionId,
+    {Uri uri}) {
+  if (uri == null) {
+    uri = defaultUri;
+  }
+
+  var response =
+      processor.get(uri.resolve('session/$sessionId')) as Map<String, dynamic>;
+  return new WebDriver(
+      processor, uri, sessionId, new UnmodifiableMapView(response));
+}
diff --git a/lib/sync_io.dart b/lib/sync_io.dart
new file mode 100644
index 0000000..7aabdbc
--- /dev/null
+++ b/lib/sync_io.dart
@@ -0,0 +1,108 @@
+// 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.sync_io;
+
+import 'dart:convert' show JSON;
+import 'dart:io' show ContentType, HttpHeaders;
+
+import 'package:sync_http/sync_http.dart';
+import 'package:webdriver/sync_core.dart' as core
+    show createDriver, fromExistingSession, WebDriver;
+import 'package:webdriver/src/sync/command_processor.dart'
+    show CommandProcessor;
+import 'package:webdriver/src/sync/exception.dart' show WebDriverException;
+
+export 'package:webdriver/sync_core.dart'
+    hide createDriver, fromExistingSession;
+
+/// Creates a WebDriver instance connected to the specified WebDriver server.
+///
+/// Note: WebDriver endpoints will be constructed using [resolve] against
+/// [uri]. Therefore, if [uri] does not end with a trailing slash, the
+/// last path component will be dropped.
+core.WebDriver createDriver({Uri uri, Map<String, dynamic> desired}) =>
+    core.createDriver(new IOCommandProcessor(), uri: uri, desired: desired);
+
+/// Creates a WebDriver instance connected to an existing session.
+///
+/// Note: WebDriver endpoints will be constructed using [resolve] against
+/// [uri]. Therefore, if [uri] does not end with a trailing slash, the
+/// last path component will be dropped.
+core.WebDriver fromExistingSession(String sessionId, {Uri uri}) =>
+    core.fromExistingSession(new IOCommandProcessor(), sessionId, uri: uri);
+
+final ContentType _contentTypeJson =
+    new ContentType("application", "json", charset: "utf-8");
+
+class IOCommandProcessor implements CommandProcessor {
+  @override
+  dynamic post(Uri uri, dynamic params, {bool value: true}) {
+    final request = SyncHttpClient.postUrl(uri);
+    _setUpRequest(request);
+    request.headers.contentType = _contentTypeJson;
+    if (params != null) {
+      var body = JSON.encode(params); // Cannot be changed from UTF8.
+      request.write(body);
+    }
+    return _processResponse(request.close(), value);
+  }
+
+  @override
+  dynamic get(Uri uri, {bool value: true}) {
+    final request = SyncHttpClient.getUrl(uri);
+    _setUpRequest(request);
+    return _processResponse(request.close(), value);
+  }
+
+  @override
+  dynamic delete(Uri uri, {bool value: true}) {
+    final request = SyncHttpClient.deleteUrl(uri);
+    _setUpRequest(request);
+    return _processResponse(request.close(), value);
+  }
+
+  @override
+  void close() {}
+
+  _processResponse(SyncHttpClientResponse response, bool value) {
+    final respDecoded = response.body;
+    Map respBody;
+    try {
+      respBody = JSON.decode(respDecoded);
+    } catch (e) {}
+
+    // TODO(staats): Update to infer protocols.
+    if (response.statusCode < 200 ||
+        response.statusCode > 299 ||
+        (respBody is Map &&
+            respBody['status'] != null &&
+            respBody['status'] != 0)) {
+      throw new WebDriverException(
+          httpStatusCode: response.statusCode,
+          httpReasonPhrase: response.reasonPhrase,
+          jsonResp: respBody);
+    }
+    if (value && respBody is Map) {
+      return respBody['value'];
+    }
+    return respBody;
+  }
+
+  void _setUpRequest(SyncHttpClientRequest request) {
+    // TODO(staats): Follow redirects.
+    request.headers.add(HttpHeaders.ACCEPT, "application/json");
+    request.headers.add(HttpHeaders.CACHE_CONTROL, "no-cache");
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 7241c25..6a274a3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -6,12 +6,13 @@
   and as such, require the use of the WebDriver remote server.
 homepage: https://github.com/google/webdriver.dart
 environment:
-  sdk: '>=1.22.0 <2.0.0'
+  sdk: '>=1.24.0-dev.0.0 <2.0.0'
 dependencies:
   archive: '^1.0.0'
   matcher: '^0.12.0'
   path: '^1.3.0'
   stack_trace: '^1.3.0'
+  sync_http: "^0.1.1"
   unittest: '^0.11.6'
 dev_dependencies:
   test: '^0.12.3'
diff --git a/test/alert_test.dart b/test/alert_test.dart
index 7172cc4..d0bdc37 100644
--- a/test/alert_test.dart
+++ b/test/alert_test.dart
@@ -16,9 +16,9 @@
 library webdriver.alert_test;
 
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
+import 'sync_io_config.dart' as config;
 
 void main() {
   group('Alert', () {
@@ -26,51 +26,54 @@
     WebElement button;
     WebElement output;
 
-    setUp(() async {
-      driver = await config.createTestDriver();
-      await driver.get(config.testPagePath);
-      button = await driver.findElement(const By.tagName('button'));
-      output = await driver.findElement(const By.id('settable'));
+    setUp(() {
+      driver = config.createTestDriver();
+      driver.get(config.testPagePath);
+      button = driver.findElement(const By.tagName('button'));
+      output = driver.findElement(const By.id('settable'));
     });
 
-    tearDown(() async {
+    tearDown(() {
       if (driver != null) {
-        await driver.quit();
+        driver.quit();
       }
       driver = null;
     });
 
     test('no alert', () {
-      expect(driver.switchTo.alert, throws);
+      try {
+        driver.switchTo.alert;
+        fail('Expected exception on no alert');
+      } on NoSuchAlertException {}
     });
 
-    test('text', () async {
-      await button.click();
-      var alert = await driver.switchTo.alert;
+    test('text', () {
+      button.click();
+      var alert = driver.switchTo.alert;
       expect(alert.text, 'button clicked');
-      await alert.dismiss();
+      alert.dismiss();
     });
 
-    test('accept', () async {
-      await button.click();
-      var alert = await driver.switchTo.alert;
-      await alert.accept();
-      expect(await output.text, startsWith('accepted'));
+    test('accept', () {
+      button.click();
+      var alert = driver.switchTo.alert;
+      alert.accept();
+      expect(output.text, startsWith('accepted'));
     });
 
-    test('dismiss', () async {
-      await button.click();
-      var alert = await driver.switchTo.alert;
-      await alert.dismiss();
-      expect(await output.text, startsWith('dismissed'));
+    test('dismiss', () {
+      button.click();
+      var alert = driver.switchTo.alert;
+      alert.dismiss();
+      expect(output.text, startsWith('dismissed'));
     });
 
-    test('sendKeys', () async {
-      await button.click();
-      Alert alert = await driver.switchTo.alert;
-      await alert.sendKeys('some keys');
-      await alert.accept();
-      expect(await output.text, endsWith('some keys'));
+    test('sendKeys', () {
+      button.click();
+      Alert alert = driver.switchTo.alert;
+      alert.sendKeys('some keys');
+      alert.accept();
+      expect(output.text, endsWith('some keys'));
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/async_alert_test.dart b/test/async_alert_test.dart
new file mode 100644
index 0000000..f40d875
--- /dev/null
+++ b/test/async_alert_test.dart
@@ -0,0 +1,76 @@
+// 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.alert_test;
+
+import 'package:test/test.dart';
+import 'package:webdriver/async_core.dart';
+
+import 'io_config.dart' as config;
+
+void main() {
+  group('Alert', () {
+    WebDriver driver;
+    WebElement button;
+    WebElement output;
+
+    setUp(() async {
+      driver = await config.createTestDriver();
+      await driver.get(config.testPagePath);
+      button = await driver.findElement(const By.tagName('button'));
+      output = await driver.findElement(const By.id('settable'));
+    });
+
+    tearDown(() async {
+      if (driver != null) {
+        await driver.quit();
+      }
+      driver = null;
+    });
+
+    test('no alert', () {
+      expect(driver.switchTo.alert, throws);
+    });
+
+    test('text', () async {
+      await button.click();
+      var alert = await driver.switchTo.alert;
+      expect(alert.text, 'button clicked');
+      await alert.dismiss();
+    });
+
+    test('accept', () async {
+      await button.click();
+      var alert = await driver.switchTo.alert;
+      await alert.accept();
+      expect(await output.text, startsWith('accepted'));
+    });
+
+    test('dismiss', () async {
+      await button.click();
+      var alert = await driver.switchTo.alert;
+      await alert.dismiss();
+      expect(await output.text, startsWith('dismissed'));
+    });
+
+    test('sendKeys', () async {
+      await button.click();
+      Alert alert = await driver.switchTo.alert;
+      await alert.sendKeys('some keys');
+      await alert.accept();
+      expect(await output.text, endsWith('some keys'));
+    });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/async_command_event_test.dart b/test/async_command_event_test.dart
new file mode 100644
index 0000000..27cd50a
--- /dev/null
+++ b/test/async_command_event_test.dart
@@ -0,0 +1,71 @@
+// 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.command_event_test;
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+import 'package:webdriver/async_core.dart';
+import 'package:webdriver/support/async.dart';
+
+import 'io_config.dart' as config;
+
+void main() {
+  group('CommandEvent', () {
+    WebDriver driver;
+
+    var events = <WebDriverCommandEvent>[];
+    var sub;
+
+    setUp(() async {
+      driver = await config.createTestDriver();
+      sub = driver.onCommand.listen(events.add);
+
+      await driver.get(config.testPagePath);
+    });
+
+    tearDown(() async {
+      sub.cancel();
+      sub = null;
+      events.clear();
+      await driver.quit();
+      driver = null;
+    });
+
+    test('handles exceptions', () async {
+      try {
+        await driver.switchTo.alert;
+      } catch (e) {}
+      await waitFor(() => events, matcher: hasLength(2));
+      expect(events[1].method, 'GET');
+      expect(events[1].endPoint, contains('alert'));
+      expect(events[1].exception, new isInstanceOf<WebDriverException>());
+      expect(events[1].result, isNull);
+      expect(events[1].startTime.isBefore(events[1].endTime), isTrue);
+      expect(events[1].stackTrace, new isInstanceOf<Chain>());
+    });
+
+    test('handles normal operation', () async {
+      await driver.findElements(const By.cssSelector('nosuchelement')).toList();
+      await waitFor(() => events, matcher: hasLength(2));
+      expect(events[1].method, 'POST');
+      expect(events[1].endPoint, contains('elements'));
+      expect(events[1].exception, isNull);
+      expect(events[1].result, hasLength(0));
+      expect(events[1].startTime.isBefore(events[1].endTime), isTrue);
+      expect(events[1].stackTrace, new isInstanceOf<Chain>());
+    });
+  }, testOn: '!js');
+}
diff --git a/test/async_keyboard_test.dart b/test/async_keyboard_test.dart
new file mode 100644
index 0000000..bd73372
--- /dev/null
+++ b/test/async_keyboard_test.dart
@@ -0,0 +1,78 @@
+// 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.keyboard_test;
+
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:webdriver/async_core.dart';
+
+import 'io_config.dart' as config;
+
+void main() {
+  group('Keyboard', () {
+    WebDriver driver;
+    WebElement textInput;
+    String ctrlCmdKey = '';
+
+    setUp(() async {
+      if (Platform.isMacOS) {
+        ctrlCmdKey = Keyboard.command;
+      } else {
+        ctrlCmdKey = Keyboard.control;
+      }
+
+      driver = await config.createTestDriver();
+      await driver.get(config.testPagePath);
+      textInput =
+          await driver.findElement(const By.cssSelector('input[type=text]'));
+      await textInput.click();
+    });
+
+    tearDown(() async {
+      if (driver != null) {
+        await driver.quit();
+      }
+      driver = null;
+    });
+
+    test('sendKeys -- once', () async {
+      await driver.keyboard.sendKeys('abcdef');
+      expect(await textInput.attributes['value'], 'abcdef');
+    });
+
+    test('sendKeys -- twice', () async {
+      await driver.keyboard.sendKeys('abc');
+      await driver.keyboard.sendKeys('def');
+      expect(await textInput.attributes['value'], 'abcdef');
+    });
+
+    test('sendKeys -- with tab', () async {
+      await driver.keyboard.sendKeys('abc${Keyboard.tab}def');
+      expect(await textInput.attributes['value'], 'abc');
+    });
+
+    // NOTE: does not work on Mac.
+    test('sendChord -- CTRL+X', () async {
+      await driver.keyboard.sendKeys('abcdef');
+      expect(await textInput.attributes['value'], 'abcdef');
+      await driver.keyboard.sendChord([ctrlCmdKey, 'a']);
+      await driver.keyboard.sendChord([ctrlCmdKey, 'x']);
+      await driver.keyboard.sendKeys('xxx');
+      expect(await textInput.attributes['value'], 'xxx');
+    });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/async_logs_test.dart b/test/async_logs_test.dart
new file mode 100644
index 0000000..625962b
--- /dev/null
+++ b/test/async_logs_test.dart
@@ -0,0 +1,52 @@
+// 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.logs_test;
+
+import 'package:test/test.dart';
+import 'package:webdriver/async_core.dart';
+
+import 'io_config.dart' as config;
+
+void main() {
+  group('Logs', () {
+    WebDriver driver;
+
+    setUp(() async {
+      Map<String, dynamic> capabilities = {
+        Capabilities.loggingPrefs: {LogType.performance: LogLevel.info}
+      };
+
+      driver =
+          await config.createTestDriver(additionalCapabilities: capabilities);
+      await driver.get(config.testPagePath);
+    });
+
+    tearDown(() async {
+      if (driver != null) {
+        await driver.quit();
+      }
+      driver = null;
+    });
+
+    test('get logs', () async {
+      List<LogEntry> logs = await driver.logs.get(LogType.performance).toList();
+      expect(logs.length, greaterThan(0));
+      logs.forEach((entry) {
+        expect(entry.level, equals(LogLevel.info));
+      });
+    });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/async_mouse_test.dart b/test/async_mouse_test.dart
new file mode 100644
index 0000000..70f8675
--- /dev/null
+++ b/test/async_mouse_test.dart
@@ -0,0 +1,80 @@
+// 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.mouse_test;
+
+import 'package:test/test.dart';
+import 'package:webdriver/async_core.dart';
+
+import 'io_config.dart' as config;
+
+void main() {
+  group('Mouse', () {
+    WebDriver driver;
+    WebElement button;
+
+    setUp(() async {
+      driver = await config.createTestDriver();
+      await driver.get(config.testPagePath);
+      button = await driver.findElement(const By.tagName('button'));
+    });
+
+    tearDown(() async {
+      if (driver != null) {
+        await driver.quit();
+      }
+      driver = null;
+    });
+
+    test('moveTo element/click', () async {
+      await driver.mouse.moveTo(element: button);
+      await driver.mouse.click();
+      var alert = await driver.switchTo.alert;
+      await alert.dismiss();
+    });
+
+    test('moveTo coordinates/click', () async {
+      var pos = await button.location;
+      await driver.mouse.moveTo(xOffset: pos.x + 5, yOffset: pos.y + 5);
+      await driver.mouse.click();
+      var alert = await driver.switchTo.alert;
+      await alert.dismiss();
+    });
+
+    test('moveTo element coordinates/click', () async {
+      await driver.mouse.moveTo(element: button, xOffset: 5, yOffset: 5);
+      await driver.mouse.click();
+      var alert = await driver.switchTo.alert;
+      await alert.dismiss();
+    });
+
+    // TODO(DrMarcII): Better up/down tests
+    test('down/up', () async {
+      await driver.mouse.moveTo(element: button);
+      await driver.mouse.down();
+      await driver.mouse.up();
+      var alert = await driver.switchTo.alert;
+      await alert.dismiss();
+    });
+
+    // TODO(DrMarcII): Better double click test
+    test('doubleClick', () async {
+      await driver.mouse.moveTo(element: button);
+      await driver.mouse.doubleClick();
+      var alert = await driver.switchTo.alert;
+      await alert.dismiss();
+    });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/async_navigation_test.dart b/test/async_navigation_test.dart
new file mode 100644
index 0000000..edea6a3
--- /dev/null
+++ b/test/async_navigation_test.dart
@@ -0,0 +1,53 @@
+// 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.navigation_test;
+
+import 'package:test/test.dart';
+import 'package:webdriver/support/async.dart';
+import 'package:webdriver/async_core.dart';
+
+import 'io_config.dart' as config;
+
+void main() {
+  group('Navigation', () {
+    WebDriver driver;
+
+    setUp(() async {
+      driver = await config.createTestDriver();
+      await driver.get(config.testPagePath);
+    });
+
+    tearDown(() async {
+      if (driver != null) {
+        await driver.quit();
+      }
+      driver = null;
+    });
+
+    test('refresh', () async {
+      var element = await driver.findElement(const By.tagName('button'));
+      await driver.navigate.refresh();
+      await waitFor(() async {
+        try {
+          await element.name;
+        } on StaleElementReferenceException {
+          return true;
+        }
+        return 'expected StaleElementReferenceException';
+      });
+    });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/async_options_test.dart b/test/async_options_test.dart
new file mode 100644
index 0000000..7c9b3a3
--- /dev/null
+++ b/test/async_options_test.dart
@@ -0,0 +1,109 @@
+// 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.options_test;
+
+import 'package:test/test.dart';
+import 'package:webdriver/async_core.dart';
+
+import 'io_config.dart' as config;
+
+void main() {
+  group('Cookies', () {
+    WebDriver driver;
+
+    setUp(() async {
+      driver = await config.createTestDriver();
+      await driver.get('http://www.google.com/ncr');
+    });
+
+    tearDown(() async {
+      if (driver != null) {
+        await driver.quit();
+      }
+      driver = null;
+    });
+
+    test('add simple cookie', () async {
+      await driver.cookies.add(new Cookie('mycookie', 'myvalue'));
+
+      bool found = false;
+      await for (var cookie in driver.cookies.all) {
+        if (cookie.name == 'mycookie') {
+          found = true;
+          expect(cookie.value, 'myvalue');
+          break;
+        }
+      }
+      expect(found, isTrue);
+    });
+
+    test('add complex cookie', () async {
+      var date = new DateTime.utc(2020);
+      await driver.cookies.add(new Cookie('mycookie', 'myvalue',
+          path: '/', domain: '.google.com', secure: false, expiry: date));
+      bool found = false;
+      await for (var cookie in driver.cookies.all) {
+        if (cookie.name == 'mycookie') {
+          found = true;
+          expect(cookie.value, 'myvalue');
+          expect(cookie.expiry, date);
+          break;
+        }
+      }
+      expect(found, isTrue);
+    });
+
+    test('delete cookie', () async {
+      await driver.cookies.add(new Cookie('mycookie', 'myvalue'));
+      await driver.cookies.delete('mycookie');
+      bool found = false;
+      await for (var cookie in driver.cookies.all) {
+        if (cookie.name == 'mycookie') {
+          found = true;
+          break;
+        }
+      }
+      expect(found, isFalse);
+    });
+
+    test('delete all cookies', () async {
+      await driver.cookies.deleteAll();
+      expect(await driver.cookies.all.toList(), isEmpty);
+    });
+  });
+
+  group('TimeOuts', () {
+    WebDriver driver;
+
+    setUp(() async {
+      driver = await config.createTestDriver();
+    });
+
+    tearDown(() async {
+      if (driver != null) {
+        await driver.quit();
+      }
+      driver = null;
+    });
+
+    // TODO(DrMarcII): Figure out how to tell if timeouts are correctly set
+    test('set all timeouts', () async {
+      await driver.timeouts.setScriptTimeout(new Duration(seconds: 5));
+      await driver.timeouts.setImplicitTimeout(new Duration(seconds: 1));
+      await driver.timeouts.setPageLoadTimeout(new Duration(seconds: 10));
+    });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/async_target_locator_test.dart b/test/async_target_locator_test.dart
new file mode 100644
index 0000000..c59a223
--- /dev/null
+++ b/test/async_target_locator_test.dart
@@ -0,0 +1,66 @@
+// 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.target_locator_test;
+
+import 'package:test/test.dart';
+import 'package:webdriver/async_core.dart';
+
+import 'io_config.dart' as config;
+
+/**
+ * Tests for switchTo.frame(). switchTo.window() and switchTo.alert are tested
+ * in other classes.
+ */
+void main() {
+  group('TargetLocator', () {
+    WebDriver driver;
+    WebElement frame;
+
+    setUp(() async {
+      driver = await config.createTestDriver();
+      await driver.get(config.testPagePath);
+      frame = await driver.findElement(const By.name('frame'));
+    });
+
+    tearDown(() async {
+      if (driver != null) {
+        await driver.quit();
+      }
+      driver = null;
+    });
+
+    test('frame index', () async {
+      await driver.switchTo.frame(0);
+      expect(await driver.pageSource, contains('this is a frame'));
+    });
+
+    test('frame name', () async {
+      await driver.switchTo.frame('frame');
+      expect(await driver.pageSource, contains('this is a frame'));
+    });
+
+    test('frame element', () async {
+      await driver.switchTo.frame(frame);
+      expect(await driver.pageSource, contains('this is a frame'));
+    });
+
+    test('root frame', () async {
+      await driver.switchTo.frame(frame);
+      await driver.switchTo.frame();
+      await driver.findElement(const By.tagName('button'));
+    });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/async_web_driver_test.dart b/test/async_web_driver_test.dart
new file mode 100644
index 0000000..c79a1fa
--- /dev/null
+++ b/test/async_web_driver_test.dart
@@ -0,0 +1,218 @@
+// 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.web_driver_test;
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:webdriver/core.dart';
+
+import 'io_config.dart' as config;
+
+void main() {
+  group('WebDriver', () {
+    group('create', () {
+      test('default', () async {
+        WebDriver driver = await config.createTestDriver();
+        await driver.get(config.testPagePath);
+        var element = await driver.findElement(const By.tagName('button'));
+        expect(await element.name, 'button');
+        await driver.quit();
+      });
+    });
+
+    group('methods', () {
+      WebDriver driver;
+
+      setUp(() async {
+        driver = await config.createTestDriver();
+        await driver.get(config.testPagePath);
+      });
+
+      tearDown(() async {
+        if (driver != null) {
+          await driver.quit();
+        }
+        driver = null;
+      });
+
+      test('get', () async {
+        await driver.get(config.testPagePath);
+        await driver.findElement(const By.tagName('button'));
+        ;
+      });
+
+      test('currentUrl', () async {
+        var url = await driver.currentUrl;
+        expect(url, anyOf(startsWith('file:'), startsWith('http:')));
+        expect(url, endsWith('test_page.html'));
+      });
+
+      test('findElement -- success', () async {
+        var element = await driver.findElement(const By.tagName('tr'));
+        expect(element, config.isWebElement);
+      });
+
+      test('findElement -- failure', () async {
+        try {
+          await driver.findElement(const By.id('non-existent-id'));
+          throw 'expected NoSuchElementException';
+        } on NoSuchElementException {}
+      });
+
+      test('findElements -- 1 found', () async {
+        var elements = await driver
+            .findElements(const By.cssSelector('input[type=text]'))
+            .toList();
+        expect(elements, hasLength(1));
+        expect(elements, everyElement(config.isWebElement));
+      });
+
+      test('findElements -- 4 found', () async {
+        var elements =
+            await driver.findElements(const By.tagName('td')).toList();
+        expect(elements, hasLength(4));
+        expect(elements, everyElement(config.isWebElement));
+      });
+
+      test('findElements -- 0 found', () async {
+        var elements =
+            await driver.findElements(const By.id('non-existent-id')).toList();
+        expect(elements, isEmpty);
+      });
+
+      test('pageSource', () async {
+        expect(await driver.pageSource, contains('<title>test_page</title>'));
+      });
+
+      test('close/windows', () async {
+        int numHandles = (await driver.windows.toList()).length;
+        await (await driver.findElement(const By.partialLinkText('Open copy')))
+            .click();
+        expect(await driver.windows.toList(), hasLength(numHandles + 1));
+        await driver.close();
+        expect(await driver.windows.toList(), hasLength(numHandles));
+      });
+
+      test('window', () async {
+        Window orig = await driver.window;
+        Window next;
+
+        await (await driver.findElement(const By.partialLinkText('Open copy')))
+            .click();
+        await for (Window window in driver.windows) {
+          if (window != orig) {
+            next = window;
+            await driver.switchTo.window(window);
+            break;
+          }
+        }
+        expect(await driver.window, equals(next));
+        await driver.close();
+      });
+
+      test('activeElement', () async {
+        var element = await driver.activeElement;
+        expect(await element.name, 'body');
+        await (await driver
+                .findElement(const By.cssSelector('input[type=text]')))
+            .click();
+        element = await driver.activeElement;
+        expect(await element.name, 'input');
+      });
+
+      test('windows', () async {
+        var windows = await driver.windows.toList();
+        expect(windows, hasLength(isPositive));
+        expect(windows, everyElement(new isInstanceOf<Window>()));
+      });
+
+      test('execute', () async {
+        WebElement button =
+            await driver.findElement(const By.tagName('button'));
+        String script = '''
+            arguments[1].textContent = arguments[0];
+            return arguments[1];''';
+        var e = await driver.execute(script, ['new text', button]);
+        expect(await e.text, 'new text');
+      });
+
+      test('executeAsync', () async {
+        WebElement button =
+            await driver.findElement(const By.tagName('button'));
+        String script = '''
+            arguments[1].textContent = arguments[0];
+            arguments[2](arguments[1]);''';
+        var e = await driver.executeAsync(script, ['new text', button]);
+        expect(await e.text, 'new text');
+      });
+
+      test('captureScreenshot', () async {
+        var screenshot = await driver.captureScreenshot().toList();
+        expect(screenshot, hasLength(isPositive));
+        expect(screenshot, everyElement(new isInstanceOf<int>()));
+      });
+
+      test('captureScreenshotAsList', () async {
+        var screenshot = await driver.captureScreenshotAsList();
+        expect(screenshot, hasLength(isPositive));
+        expect(screenshot, everyElement(new isInstanceOf<int>()));
+      });
+
+      test('captureScreenshotAsBase64', () async {
+        var screenshot = await driver.captureScreenshotAsBase64();
+        expect(screenshot, hasLength(isPositive));
+        expect(screenshot, new isInstanceOf<String>());
+      });
+
+      test('future based event listeners work with script timeouts', () async {
+        driver.addEventListener((WebDriverCommandEvent e) async {
+          return await new Future.delayed(
+              new Duration(milliseconds: 1000), (() {}));
+        });
+
+        try {
+          driver.timeouts.setScriptTimeout(new Duration(seconds: 1));
+          await driver.executeAsync('', []);
+          fail('Did not throw timeout as expected');
+        } catch (e) {
+          expect(e.toString(), contains('asynchronous script timeout'));
+        }
+      });
+
+      test('future based event listeners ordered appropriately', () async {
+        var eventList = new List<int>();
+        int millisDelay = 2000;
+        int current = 0;
+        driver.addEventListener((WebDriverCommandEvent e) async {
+          return await new Future.delayed(
+              new Duration(milliseconds: millisDelay), (() {
+            eventList.add(current++);
+            millisDelay = (millisDelay / 2).round();
+          }));
+        });
+
+        for (int i = 0; i < 10; i++) {
+          await driver.title; // GET request.
+        }
+        expect(eventList, hasLength(10));
+        for (int i = 0; i < 10; i++) {
+          expect(eventList[i], i);
+        }
+      });
+    });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/async_web_element_test.dart b/test/async_web_element_test.dart
new file mode 100644
index 0000000..7a9ec6b
--- /dev/null
+++ b/test/async_web_element_test.dart
@@ -0,0 +1,192 @@
+// 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.web_element_test;
+
+import 'package:test/test.dart';
+import 'package:webdriver/async_core.dart';
+
+import 'io_config.dart' as config;
+
+void main() {
+  group('WebElement', () {
+    WebDriver driver;
+    WebElement table;
+    WebElement button;
+    WebElement form;
+    WebElement textInput;
+    WebElement checkbox;
+    WebElement disabled;
+    WebElement invisible;
+
+    setUp(() async {
+      driver = await config.createTestDriver();
+      await driver.get(config.testPagePath);
+      table = await driver.findElement(const By.tagName('table'));
+      button = await driver.findElement(const By.tagName('button'));
+      form = await driver.findElement(const By.tagName('form'));
+      textInput =
+          await driver.findElement(const By.cssSelector('input[type=text]'));
+      checkbox = await driver
+          .findElement(const By.cssSelector('input[type=checkbox]'));
+      disabled = await driver
+          .findElement(const By.cssSelector('input[type=password]'));
+      invisible = await driver.findElement(const By.tagName('div'));
+    });
+
+    tearDown(() async {
+      if (driver != null) {
+        await driver.quit();
+      }
+      driver = null;
+    });
+
+    test('click', () async {
+      await button.click();
+      var alert = await driver.switchTo.alert;
+      await alert.accept();
+    });
+
+    test('submit', () async {
+      await form.submit();
+      var alert = await driver.switchTo.alert;
+      expect(alert.text, 'form submitted');
+      await alert.accept();
+    });
+
+    test('sendKeys', () async {
+      await textInput.sendKeys('some keys');
+      expect(await textInput.attributes['value'], 'some keys');
+    });
+
+    test('clear', () async {
+      await textInput.sendKeys('some keys');
+      await textInput.clear();
+      expect(await textInput.attributes['value'], '');
+    });
+
+    test('enabled', () async {
+      expect(await table.enabled, isTrue);
+      expect(await button.enabled, isTrue);
+      expect(await form.enabled, isTrue);
+      expect(await textInput.enabled, isTrue);
+      expect(await checkbox.enabled, isTrue);
+      expect(await disabled.enabled, isFalse);
+    });
+
+    test('displayed', () async {
+      expect(await table.displayed, isTrue);
+      expect(await button.displayed, isTrue);
+      expect(await form.displayed, isTrue);
+      expect(await textInput.displayed, isTrue);
+      expect(await checkbox.displayed, isTrue);
+      expect(await disabled.displayed, isTrue);
+      expect(await invisible.displayed, isFalse);
+    });
+
+    test('location -- table', () async {
+      var location = await table.location;
+      expect(location, config.isPoint);
+      expect(location.x, isNonNegative);
+      expect(location.y, isNonNegative);
+    });
+
+    test('location -- invisible', () async {
+      var location = await invisible.location;
+      expect(location, config.isPoint);
+      expect(location.x, 0);
+      expect(location.y, 0);
+    });
+
+    test('size -- table', () async {
+      var size = await table.size;
+      expect(size, config.isRectangle);
+      expect(size.width, isNonNegative);
+      expect(size.height, isNonNegative);
+    });
+
+    test('size -- invisible', () async {
+      var size = await invisible.size;
+      expect(size, config.isRectangle);
+      // TODO(DrMarcII): I thought these should be 0
+      expect(size.width, isNonNegative);
+      expect(size.height, isNonNegative);
+    });
+
+    test('name', () async {
+      expect(await table.name, 'table');
+      expect(await button.name, 'button');
+      expect(await form.name, 'form');
+      expect(await textInput.name, 'input');
+    });
+
+    test('text', () async {
+      expect(await table.text, 'r1c1 r1c2\nr2c1 r2c2');
+      expect(await button.text, 'button');
+      expect(await invisible.text, '');
+    });
+
+    test('findElement -- success', () async {
+      var element = await table.findElement(const By.tagName('tr'));
+      expect(element, config.isWebElement);
+    });
+
+    test('findElement -- failure', () async {
+      try {
+        await button.findElement(const By.tagName('tr'));
+        throw 'Expected NoSuchElementException';
+      } on NoSuchElementException {}
+    });
+
+    test('findElements -- 1 found', () async {
+      var elements = await form
+          .findElements(const By.cssSelector('input[type=text]'))
+          .toList();
+      expect(elements, hasLength(1));
+      expect(elements, everyElement(config.isWebElement));
+    });
+
+    test('findElements -- 4 found', () async {
+      var elements = await table.findElements(const By.tagName('td')).toList();
+      expect(elements, hasLength(4));
+      expect(elements, everyElement(config.isWebElement));
+    });
+
+    test('findElements -- 0 found', () async {
+      var elements = await form.findElements(const By.tagName('td')).toList();
+      expect(elements, isEmpty);
+    });
+
+    test('attributes', () async {
+      expect(await table.attributes['id'], 'table1');
+      expect(await table.attributes['non-standard'], 'a non standard attr');
+      expect(await table.attributes['disabled'], isNull);
+      expect(await disabled.attributes['disabled'], 'true');
+    });
+
+    test('cssProperties', () async {
+      expect(await invisible.cssProperties['display'], 'none');
+      expect(await invisible.cssProperties['background-color'],
+          'rgba(255, 0, 0, 1)');
+      expect(await invisible.cssProperties['direction'], 'ltr');
+    });
+
+    test('equals', () async {
+      expect(await invisible.equals(disabled), isFalse);
+      var element = await driver.findElement(const By.cssSelector('table'));
+      expect(await element.equals(table), isTrue);
+    });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/async_window_test.dart b/test/async_window_test.dart
new file mode 100644
index 0000000..913c440
--- /dev/null
+++ b/test/async_window_test.dart
@@ -0,0 +1,75 @@
+// 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.window_test;
+
+import 'dart:math' show Point, Rectangle;
+
+import 'package:test/test.dart';
+import 'package:webdriver/support/async.dart';
+import 'package:webdriver/async_core.dart';
+
+import 'io_config.dart' as config;
+
+void main() {
+  group('Window', () {
+    WebDriver driver;
+
+    setUp(() async {
+      driver = await config.createTestDriver();
+    });
+
+    tearDown(() async {
+      if (driver != null) {
+        await driver.quit();
+      }
+      driver = null;
+    });
+
+    test('size', () async {
+      var window = await driver.window;
+      var size = const Rectangle<int>(0, 0, 600, 400);
+      await window.setSize(size);
+      expect(await window.size, size);
+    });
+
+    test('location', () async {
+      var window = await driver.window;
+      var position = const Point<int>(100, 200);
+      await window.setLocation(position);
+      expect(await window.location, position);
+    });
+
+    // May not work on some OS/browser combinations (notably Mac OS X).
+    test('maximize', () async {
+      var window = await driver.window;
+      await window.setSize(const Rectangle<int>(0, 0, 300, 200));
+      await window.setLocation(const Point<int>(100, 200));
+      await window.maximize();
+
+      // maximizing can take some time
+      await waitFor(() async => (await window.size).height,
+          matcher: greaterThan(200));
+
+      var location = await window.location;
+      var size = await window.size;
+      // Changed from `lessThan(100)` to pass the test on Mac.
+      expect(location.x, lessThanOrEqualTo(100));
+      expect(location.y, lessThan(200));
+      expect(size.height, greaterThan(200));
+      expect(size.width, greaterThan(300));
+    }, skip: 'unreliable');
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/basic_sync_test.dart b/test/basic_sync_test.dart
new file mode 100644
index 0000000..b2a4864
--- /dev/null
+++ b/test/basic_sync_test.dart
@@ -0,0 +1,49 @@
+// Copyright 2017 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.async_test;
+
+import 'package:test/test.dart';
+import 'package:webdriver/sync_io.dart';
+
+import 'sync_io_config.dart' as config;
+
+void main() {
+  group('Sync IO', () {
+    WebDriver driver;
+
+    setUp(() {
+      driver = config.createTestDriver();
+    });
+
+    tearDown(() {
+      if (driver != null) {
+        driver.quit();
+      }
+      driver = null;
+    });
+
+    test('can do basic post', () {
+      driver.get(config.testPagePath); // This is POST to WebDriver.
+    });
+
+    test('can do basic get', () {
+      driver.title; // This is a GET request.
+    });
+
+    test('can do basic delete', () {
+      driver.close(); // This is a DELETE request.
+    });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
+}
diff --git a/test/command_event_test.dart b/test/command_event_test.dart
index 44ee8f0..1cec128 100644
--- a/test/command_event_test.dart
+++ b/test/command_event_test.dart
@@ -17,38 +17,33 @@
 
 import 'package:stack_trace/stack_trace.dart';
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
-import 'package:webdriver/support/async.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
+import 'sync_io_config.dart' as config;
 
 void main() {
   group('CommandEvent', () {
     WebDriver driver;
 
     var events = <WebDriverCommandEvent>[];
-    var sub;
 
-    setUp(() async {
-      driver = await config.createTestDriver();
-      sub = driver.onCommand.listen(events.add);
-
-      await driver.get(config.testPagePath);
+    setUp(() {
+      driver = config.createTestDriver();
+      driver.addEventListener(events.add);
+      driver.get(config.testPagePath);
     });
 
-    tearDown(() async {
-      sub.cancel();
-      sub = null;
+    tearDown(() {
       events.clear();
-      await driver.quit();
+      driver.quit();
       driver = null;
     });
 
-    test('handles exceptions', () async {
+    test('handles exceptions', () {
       try {
-        await driver.switchTo.alert;
-      } catch (e) {}
-      await waitFor(() => events, matcher: hasLength(2));
+        driver.switchTo.alert;
+        fail('Expected exception on no alert');
+      } catch (NoSuchAlertException) {}
       expect(events[1].method, 'GET');
       expect(events[1].endPoint, contains('alert'));
       expect(events[1].exception, new isInstanceOf<WebDriverException>());
@@ -57,9 +52,8 @@
       expect(events[1].stackTrace, new isInstanceOf<Chain>());
     });
 
-    test('handles normal operation', () async {
-      await driver.findElements(const By.cssSelector('nosuchelement')).toList();
-      await waitFor(() => events, matcher: hasLength(2));
+    test('handles normal operation', () {
+      driver.findElements(const By.cssSelector('nosuchelement')).toList();
       expect(events[1].method, 'POST');
       expect(events[1].endPoint, contains('elements'));
       expect(events[1].exception, isNull);
@@ -67,5 +61,5 @@
       expect(events[1].startTime.isBefore(events[1].endTime), isTrue);
       expect(events[1].stackTrace, new isInstanceOf<Chain>());
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/io_config.dart b/test/io_config.dart
index 85b4861..feb1e12 100644
--- a/test/io_config.dart
+++ b/test/io_config.dart
@@ -15,11 +15,12 @@
 library io_test_util;
 
 import 'dart:async' show Future;
-import 'dart:io' show FileSystemEntity, Platform;
+import 'dart:io' show Platform;
 
-import 'package:path/path.dart' as path;
-import 'package:webdriver/core.dart' show Capabilities, WebDriver;
-import 'package:webdriver/io.dart' show createDriver;
+import 'package:webdriver/async_core.dart' show Capabilities, WebDriver;
+import 'package:webdriver/async_io.dart' show createDriver;
+
+export 'test_util.dart';
 
 Future<WebDriver> createTestDriver(
     {Map<String, dynamic> additionalCapabilities}) {
@@ -46,12 +47,3 @@
 
   return createDriver(desired: capabilities);
 }
-
-String get testPagePath {
-  String testPagePath = path.absolute('test', 'test_page.html');
-  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.');
-  }
-  return path.toUri(testPagePath).toString();
-}
diff --git a/test/keyboard_test.dart b/test/keyboard_test.dart
index 7e16923..9844856 100644
--- a/test/keyboard_test.dart
+++ b/test/keyboard_test.dart
@@ -18,9 +18,9 @@
 import 'dart:io';
 
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
+import 'sync_io_config.dart' as config;
 
 void main() {
   group('Keyboard', () {
@@ -28,50 +28,50 @@
     WebElement textInput;
     String ctrlCmdKey = '';
 
-    setUp(() async {
+    setUp(() {
       if (Platform.isMacOS) {
         ctrlCmdKey = Keyboard.command;
       } else {
         ctrlCmdKey = Keyboard.control;
       }
 
-      driver = await config.createTestDriver();
-      await driver.get(config.testPagePath);
-      textInput =
-          await driver.findElement(const By.cssSelector('input[type=text]'));
-      await textInput.click();
+      driver = config.createTestDriver();
+      driver.get(config.testPagePath);
+      textInput = driver.findElement(const By.cssSelector('input[type=text]'));
+      textInput.click();
     });
 
-    tearDown(() async {
+    tearDown(() {
       if (driver != null) {
-        await driver.quit();
+        driver.quit();
       }
       driver = null;
     });
 
-    test('sendKeys -- once', () async {
-      await driver.keyboard.sendKeys('abcdef');
-      expect(await textInput.attributes['value'], 'abcdef');
+    test('sendKeys -- once', () {
+      driver.keyboard.sendKeys('abcdef');
+      expect(textInput.attributes['value'], 'abcdef');
     });
 
-    test('sendKeys -- twice', () async {
-      await driver.keyboard.sendKeys('abc');
-      await driver.keyboard.sendKeys('def');
-      expect(await textInput.attributes['value'], 'abcdef');
+    test('sendKeys -- twice', () {
+      driver.keyboard.sendKeys('abc');
+      driver.keyboard.sendKeys('def');
+      expect(textInput.attributes['value'], 'abcdef');
     });
 
-    test('sendKeys -- with tab', () async {
-      await driver.keyboard.sendKeys('abc${Keyboard.tab}def');
-      expect(await textInput.attributes['value'], 'abc');
+    test('sendKeys -- with tab', () {
+      driver.keyboard.sendKeys('abc${Keyboard.tab}def');
+      expect(textInput.attributes['value'], 'abc');
     });
 
-    test('sendChord -- CTRL+X', () async {
-      await driver.keyboard.sendKeys('abcdef');
-      expect(await textInput.attributes['value'], 'abcdef');
-      await driver.keyboard.sendChord([ctrlCmdKey, 'a']);
-      await driver.keyboard.sendChord([ctrlCmdKey, 'x']);
-      await driver.keyboard.sendKeys('xxx');
-      expect(await textInput.attributes['value'], 'xxx');
+    // NOTE: does not work on Mac.
+    test('sendChord -- CTRL+X', () {
+      driver.keyboard.sendKeys('abcdef');
+      expect(textInput.attributes['value'], 'abcdef');
+      driver.keyboard.sendChord([ctrlCmdKey, 'a']);
+      driver.keyboard.sendChord([ctrlCmdKey, 'x']);
+      driver.keyboard.sendKeys('xxx');
+      expect(textInput.attributes['value'], 'xxx');
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/logs_test.dart b/test/logs_test.dart
index ee42496..c19190c 100644
--- a/test/logs_test.dart
+++ b/test/logs_test.dart
@@ -16,37 +16,36 @@
 library webdriver.logs_test;
 
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
+import 'sync_io_config.dart' as config;
 
 void main() {
   group('Logs', () {
     WebDriver driver;
 
-    setUp(() async {
+    setUp(() {
       Map<String, dynamic> capabilities = {
         Capabilities.loggingPrefs: {LogType.performance: LogLevel.info}
       };
 
-      driver =
-          await config.createTestDriver(additionalCapabilities: capabilities);
-      await driver.get(config.testPagePath);
+      driver = config.createTestDriver(additionalCapabilities: capabilities);
+      driver.get(config.testPagePath);
     });
 
-    tearDown(() async {
+    tearDown(() {
       if (driver != null) {
-        await driver.quit();
+        driver.quit();
       }
       driver = null;
     });
 
-    test('get logs', () async {
-      List<LogEntry> logs = await driver.logs.get(LogType.performance).toList();
+    test('get logs', () {
+      List<LogEntry> logs = driver.logs.get(LogType.performance).toList();
       expect(logs.length, greaterThan(0));
       logs.forEach((entry) {
         expect(entry.level, equals(LogLevel.info));
       });
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/mouse_test.dart b/test/mouse_test.dart
index b86c77c..88af1ce 100644
--- a/test/mouse_test.dart
+++ b/test/mouse_test.dart
@@ -16,65 +16,65 @@
 library webdriver.mouse_test;
 
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
+import 'sync_io_config.dart' as config;
 
 void main() {
   group('Mouse', () {
     WebDriver driver;
     WebElement button;
 
-    setUp(() async {
-      driver = await config.createTestDriver();
-      await driver.get(config.testPagePath);
-      button = await driver.findElement(const By.tagName('button'));
+    setUp(() {
+      driver = config.createTestDriver();
+      driver.get(config.testPagePath);
+      button = driver.findElement(const By.tagName('button'));
     });
 
-    tearDown(() async {
+    tearDown(() {
       if (driver != null) {
-        await driver.quit();
+        driver.quit();
       }
       driver = null;
     });
 
-    test('moveTo element/click', () async {
-      await driver.mouse.moveTo(element: button);
-      await driver.mouse.click();
-      var alert = await driver.switchTo.alert;
-      await alert.dismiss();
+    test('moveTo element/click', () {
+      driver.mouse.moveTo(element: button);
+      driver.mouse.click();
+      var alert = driver.switchTo.alert;
+      alert.dismiss();
     });
 
-    test('moveTo coordinates/click', () async {
-      var pos = await button.location;
-      await driver.mouse.moveTo(xOffset: pos.x + 5, yOffset: pos.y + 5);
-      await driver.mouse.click();
-      var alert = await driver.switchTo.alert;
-      await alert.dismiss();
+    test('moveTo coordinates/click', () {
+      var pos = button.location;
+      driver.mouse.moveTo(xOffset: pos.x + 5, yOffset: pos.y + 5);
+      driver.mouse.click();
+      var alert = driver.switchTo.alert;
+      alert.dismiss();
     });
 
-    test('moveTo element coordinates/click', () async {
-      await driver.mouse.moveTo(element: button, xOffset: 5, yOffset: 5);
-      await driver.mouse.click();
-      var alert = await driver.switchTo.alert;
-      await alert.dismiss();
+    test('moveTo element coordinates/click', () {
+      driver.mouse.moveTo(element: button, xOffset: 5, yOffset: 5);
+      driver.mouse.click();
+      var alert = driver.switchTo.alert;
+      alert.dismiss();
     });
 
     // TODO(DrMarcII): Better up/down tests
-    test('down/up', () async {
-      await driver.mouse.moveTo(element: button);
-      await driver.mouse.down();
-      await driver.mouse.up();
-      var alert = await driver.switchTo.alert;
-      await alert.dismiss();
+    test('down/up', () {
+      driver.mouse.moveTo(element: button);
+      driver.mouse.down();
+      driver.mouse.up();
+      var alert = driver.switchTo.alert;
+      alert.dismiss();
     });
 
     // TODO(DrMarcII): Better double click test
-    test('doubleClick', () async {
-      await driver.mouse.moveTo(element: button);
-      await driver.mouse.doubleClick();
-      var alert = await driver.switchTo.alert;
-      await alert.dismiss();
+    test('doubleClick', () {
+      driver.mouse.moveTo(element: button);
+      driver.mouse.doubleClick();
+      var alert = driver.switchTo.alert;
+      alert.dismiss();
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/navigation_test.dart b/test/navigation_test.dart
index 9e29e0e..c102b80 100644
--- a/test/navigation_test.dart
+++ b/test/navigation_test.dart
@@ -16,38 +16,35 @@
 library webdriver.navigation_test;
 
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
-import 'package:webdriver/support/async.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
+import 'sync_io_config.dart' as config;
 
 void main() {
   group('Navigation', () {
     WebDriver driver;
 
-    setUp(() async {
-      driver = await config.createTestDriver();
-      await driver.get(config.testPagePath);
+    setUp(() {
+      driver = config.createTestDriver();
+      driver.get(config.testPagePath);
     });
 
-    tearDown(() async {
+    tearDown(() {
       if (driver != null) {
-        await driver.quit();
+        driver.quit();
       }
       driver = null;
     });
 
-    test('refresh', () async {
-      var element = await driver.findElement(const By.tagName('button'));
-      await driver.navigate.refresh();
-      await waitFor(() async {
-        try {
-          await element.name;
-        } on StaleElementReferenceException {
-          return true;
-        }
-        return 'expected StaleElementReferenceException';
-      });
+    test('refresh', () {
+      var element = driver.findElement(const By.tagName('button'));
+      driver.navigate.refresh();
+      try {
+        element.name;
+      } on StaleElementReferenceException {
+        return true;
+      }
+      return 'expected StaleElementReferenceException';
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/options_test.dart b/test/options_test.dart
index 3d7df49..63645c4 100644
--- a/test/options_test.dart
+++ b/test/options_test.dart
@@ -16,31 +16,31 @@
 library webdriver.options_test;
 
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
+import 'sync_io_config.dart' as config;
 
 void main() {
   group('Cookies', () {
     WebDriver driver;
 
-    setUp(() async {
-      driver = await config.createTestDriver();
-      await driver.get('http://www.google.com/ncr');
+    setUp(() {
+      driver = config.createTestDriver();
+      driver.get('http://www.google.com/ncr');
     });
 
-    tearDown(() async {
+    tearDown(() {
       if (driver != null) {
-        await driver.quit();
+        driver.quit();
       }
       driver = null;
     });
 
-    test('add simple cookie', () async {
-      await driver.cookies.add(new Cookie('mycookie', 'myvalue'));
+    test('add simple cookie', () {
+      driver.cookies.add(new Cookie('mycookie', 'myvalue'));
 
       bool found = false;
-      await for (var cookie in driver.cookies.all) {
+      for (var cookie in driver.cookies.all) {
         if (cookie.name == 'mycookie') {
           found = true;
           expect(cookie.value, 'myvalue');
@@ -50,12 +50,12 @@
       expect(found, isTrue);
     });
 
-    test('add complex cookie', () async {
+    test('add complex cookie', () {
       var date = new DateTime.utc(2020);
-      await driver.cookies.add(new Cookie('mycookie', 'myvalue',
+      driver.cookies.add(new Cookie('mycookie', 'myvalue',
           path: '/', domain: '.google.com', secure: false, expiry: date));
       bool found = false;
-      await for (var cookie in driver.cookies.all) {
+      for (var cookie in driver.cookies.all) {
         if (cookie.name == 'mycookie') {
           found = true;
           expect(cookie.value, 'myvalue');
@@ -66,11 +66,11 @@
       expect(found, isTrue);
     });
 
-    test('delete cookie', () async {
-      await driver.cookies.add(new Cookie('mycookie', 'myvalue'));
-      await driver.cookies.delete('mycookie');
+    test('delete cookie', () {
+      driver.cookies.add(new Cookie('mycookie', 'myvalue'));
+      driver.cookies.delete('mycookie');
       bool found = false;
-      await for (var cookie in driver.cookies.all) {
+      for (var cookie in driver.cookies.all) {
         if (cookie.name == 'mycookie') {
           found = true;
           break;
@@ -79,31 +79,31 @@
       expect(found, isFalse);
     });
 
-    test('delete all cookies', () async {
-      await driver.cookies.deleteAll();
-      expect(await driver.cookies.all.toList(), isEmpty);
-    }, skip: 'ureliable');
+    test('delete all cookies', () {
+      driver.cookies.deleteAll();
+      expect(driver.cookies.all.toList(), isEmpty);
+    }, skip: 'unreliable');
   });
 
   group('TimeOuts', () {
     WebDriver driver;
 
-    setUp(() async {
-      driver = await config.createTestDriver();
+    setUp(() {
+      driver = config.createTestDriver();
     });
 
-    tearDown(() async {
+    tearDown(() {
       if (driver != null) {
-        await driver.quit();
+        driver.quit();
       }
       driver = null;
     });
 
     // TODO(DrMarcII): Figure out how to tell if timeouts are correctly set
-    test('set all timeouts', () async {
-      await driver.timeouts.setScriptTimeout(new Duration(seconds: 5));
-      await driver.timeouts.setImplicitTimeout(new Duration(seconds: 1));
-      await driver.timeouts.setPageLoadTimeout(new Duration(seconds: 10));
+    test('set all timeouts', () {
+      driver.timeouts.setScriptTimeout(new Duration(seconds: 5));
+      driver.timeouts.setImplicitTimeout(new Duration(seconds: 1));
+      driver.timeouts.setPageLoadTimeout(new Duration(seconds: 10));
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/support/async_test.dart b/test/support/async_test.dart
index d185fd6..a480880 100644
--- a/test/support/async_test.dart
+++ b/test/support/async_test.dart
@@ -200,7 +200,7 @@
       }
       expect(exception, isNotNull);
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
 
 /// FakeClock for testing waitFor functionality.
diff --git a/test/support/firefox_profile_test.dart b/test/support/firefox_profile_test.dart
index 1af8fb5..a6a1232 100644
--- a/test/support/firefox_profile_test.dart
+++ b/test/support/firefox_profile_test.dart
@@ -20,7 +20,7 @@
 
 import 'package:archive/archive.dart' show Archive, ArchiveFile, ZipDecoder;
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
+import 'package:webdriver/async_core.dart';
 import 'package:webdriver/support/firefox_profile.dart';
 
 void main() {
@@ -169,7 +169,7 @@
           anyElement((PrefsOption o) =>
               o.name == Capabilities.hasNativeEvents && o.value == true));
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
 
 Archive unpackArchiveData(Map profileData) {
diff --git a/test/support/forwarder_test.dart b/test/support/forwarder_test.dart
index b5e9e81..5a84178 100644
--- a/test/support/forwarder_test.dart
+++ b/test/support/forwarder_test.dart
@@ -152,5 +152,5 @@
     test('window close', () async {
       await forwardedDriver.close();
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/sync_io_config.dart b/test/sync_io_config.dart
new file mode 100644
index 0000000..07bd886
--- /dev/null
+++ b/test/sync_io_config.dart
@@ -0,0 +1,47 @@
+// Copyright 2017 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 io_test_util;
+
+import 'dart:io' show Platform;
+
+import 'package:webdriver/sync_core.dart' show Capabilities, WebDriver;
+import 'package:webdriver/sync_io.dart' show createDriver;
+
+export 'test_util.dart';
+
+WebDriver createTestDriver({Map<String, dynamic> additionalCapabilities}) {
+  var 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: additionalCapabilities);
+}
diff --git a/test/target_locator_test.dart b/test/target_locator_test.dart
index 5bf65ce..35c86ad 100644
--- a/test/target_locator_test.dart
+++ b/test/target_locator_test.dart
@@ -16,9 +16,9 @@
 library webdriver.target_locator_test;
 
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
+import 'sync_io_config.dart' as config;
 
 /**
  * Tests for switchTo.frame(). switchTo.window() and switchTo.alert are tested
@@ -29,38 +29,38 @@
     WebDriver driver;
     WebElement frame;
 
-    setUp(() async {
-      driver = await config.createTestDriver();
-      await driver.get(config.testPagePath);
-      frame = await driver.findElement(const By.name('frame'));
+    setUp(() {
+      driver = config.createTestDriver();
+      driver.get(config.testPagePath);
+      frame = driver.findElement(const By.name('frame'));
     });
 
-    tearDown(() async {
+    tearDown(() {
       if (driver != null) {
-        await driver.quit();
+        driver.quit();
       }
       driver = null;
     });
 
-    test('frame index', () async {
-      await driver.switchTo.frame(0);
-      expect(await driver.pageSource, contains('this is a frame'));
+    test('frame index', () {
+      driver.switchTo.frame(0);
+      expect(driver.pageSource, contains('this is a frame'));
     });
 
-    test('frame name', () async {
-      await driver.switchTo.frame('frame');
-      expect(await driver.pageSource, contains('this is a frame'));
+    test('frame name', () {
+      driver.switchTo.frame('frame');
+      expect(driver.pageSource, contains('this is a frame'));
     });
 
-    test('frame element', () async {
-      await driver.switchTo.frame(frame);
-      expect(await driver.pageSource, contains('this is a frame'));
+    test('frame element', () {
+      driver.switchTo.frame(frame);
+      expect(driver.pageSource, contains('this is a frame'));
     });
 
-    test('root frame', () async {
-      await driver.switchTo.frame(frame);
-      await driver.switchTo.frame();
-      await driver.findElement(const By.tagName('button'));
+    test('root frame', () {
+      driver.switchTo.frame(frame);
+      driver.switchTo.frame();
+      driver.findElement(const By.tagName('button'));
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/test_util.dart b/test/test_util.dart
index 6a0646a..0a1722a 100644
--- a/test/test_util.dart
+++ b/test/test_util.dart
@@ -15,10 +15,23 @@
 library webdriver_test_util;
 
 import 'dart:math' show Point, Rectangle;
+import 'dart:io' show FileSystemEntity;
 
 import 'package:matcher/matcher.dart' show isInstanceOf, Matcher;
-import 'package:webdriver/core.dart' show WebElement;
+import 'package:path/path.dart' as path;
+import 'package:webdriver/async_core.dart' as async_core;
+import 'package:webdriver/sync_core.dart' as sync_core;
 
-final Matcher isWebElement = new isInstanceOf<WebElement>();
+final Matcher isWebElement = new isInstanceOf<async_core.WebElement>();
+final Matcher isSyncWebElement = new isInstanceOf<sync_core.WebElement>();
 final Matcher isRectangle = new isInstanceOf<Rectangle<int>>();
 final Matcher isPoint = new isInstanceOf<Point<int>>();
+
+String get testPagePath {
+  String testPagePath = path.absolute('test', 'test_page.html');
+  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.');
+  }
+  return path.toUri(testPagePath).toString();
+}
diff --git a/test/web_driver_test.dart b/test/web_driver_test.dart
index 69ea352..3569bf6 100644
--- a/test/web_driver_test.dart
+++ b/test/web_driver_test.dart
@@ -15,202 +15,179 @@
 @TestOn("vm")
 library webdriver.web_driver_test;
 
-import 'dart:async';
-
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
-import 'test_util.dart';
+import 'sync_io_config.dart' as config;
 
 void main() {
   group('WebDriver', () {
-    group(
-      'create',
-      () {
-        test('default', () async {
-          WebDriver driver = await config.createTestDriver();
-          await driver.get(config.testPagePath);
-          var element = await driver.findElement(const By.tagName('button'));
-          expect(await element.name, 'button');
-          await driver.quit();
-        });
-      },
-    );
+    group('create', () {
+      test('default', () {
+        WebDriver driver = config.createTestDriver();
+        driver.get(config.testPagePath);
+        var element = driver.findElement(const By.tagName('button'));
+        expect(element.name, 'button');
+        driver.quit();
+      });
+    });
 
     group('methods', () {
       WebDriver driver;
 
-      setUp(() async {
-        driver = await config.createTestDriver();
-        await driver.get(config.testPagePath);
+      setUp(() {
+        driver = config.createTestDriver();
+        driver.get(config.testPagePath);
       });
 
-      tearDown(() async {
+      tearDown(() {
         if (driver != null) {
-          await driver.quit();
+          driver.quit();
         }
         driver = null;
       });
 
-      test('get', () async {
-        await driver.get(config.testPagePath);
-        await driver.findElement(const By.tagName('button'));
+      test('get', () {
+        driver.get(config.testPagePath);
+        driver.findElement(const By.tagName('button'));
         ;
       });
 
-      test('currentUrl', () async {
-        var url = await driver.currentUrl;
+      test('currentUrl', () {
+        var url = driver.currentUrl;
         expect(url, anyOf(startsWith('file:'), startsWith('http:')));
         expect(url, endsWith('test_page.html'));
       });
 
-      test('findElement -- success', () async {
-        var element = await driver.findElement(const By.tagName('tr'));
-        expect(element, isWebElement);
+      test('findElement -- success', () {
+        var element = driver.findElement(const By.tagName('tr'));
+        expect(element, config.isSyncWebElement);
       });
 
-      test('findElement -- failure', () async {
+      test('findElement -- failure', () {
         try {
-          await driver.findElement(const By.id('non-existent-id'));
+          driver.findElement(const By.id('non-existent-id'));
           throw 'expected NoSuchElementException';
         } on NoSuchElementException {}
       });
 
-      test('findElements -- 1 found', () async {
-        var elements = await driver
+      test('findElements -- 1 found', () {
+        var elements = driver
             .findElements(const By.cssSelector('input[type=text]'))
             .toList();
         expect(elements, hasLength(1));
-        expect(elements, everyElement(isWebElement));
+        expect(elements, everyElement(config.isSyncWebElement));
       });
 
-      test('findElements -- 4 found', () async {
-        var elements =
-            await driver.findElements(const By.tagName('td')).toList();
+      test('findElements -- 4 found', () {
+        var elements = driver.findElements(const By.tagName('td')).toList();
         expect(elements, hasLength(4));
-        expect(elements, everyElement(isWebElement));
+        expect(elements, everyElement(config.isSyncWebElement));
       });
 
-      test('findElements -- 0 found', () async {
+      test('findElements -- 0 found', () {
         var elements =
-            await driver.findElements(const By.id('non-existent-id')).toList();
+            driver.findElements(const By.id('non-existent-id')).toList();
         expect(elements, isEmpty);
       });
 
-      test('pageSource', () async {
-        expect(await driver.pageSource, contains('<title>test_page</title>'));
+      test('pageSource', () {
+        expect(driver.pageSource, contains('<title>test_page</title>'));
       });
 
-      test('close/windows', () async {
-        int numHandles = (await driver.windows.toList()).length;
-        await (await driver.findElement(const By.partialLinkText('Open copy')))
-            .click();
-        expect(await driver.windows.toList(), hasLength(numHandles + 1));
-        await driver.close();
-        expect(await driver.windows.toList(), hasLength(numHandles));
+      test('close/windows', () {
+        int numHandles = (driver.windows.toList()).length;
+        (driver.findElement(const By.partialLinkText('Open copy'))).click();
+        expect(driver.windows.toList(), hasLength(numHandles + 1));
+        driver.close();
+        expect(driver.windows.toList(), hasLength(numHandles));
       });
 
-      test('window', () async {
-        Window orig = await driver.window;
+      test('window', () {
+        Window orig = driver.window;
         Window next;
 
-        await (await driver.findElement(const By.partialLinkText('Open copy')))
-            .click();
-        await for (Window window in driver.windows) {
+        (driver.findElement(const By.partialLinkText('Open copy'))).click();
+        for (Window window in driver.windows) {
           if (window != orig) {
             next = window;
-            await driver.switchTo.window(window);
+            driver.switchTo.window(window);
             break;
           }
         }
-        expect(await driver.window, equals(next));
-        await driver.close();
+        expect(driver.window, equals(next));
+        driver.close();
       });
 
-      test('activeElement', () async {
-        var element = await driver.activeElement;
-        expect(await element.name, 'body');
-        await (await driver
-                .findElement(const By.cssSelector('input[type=text]')))
-            .click();
-        element = await driver.activeElement;
-        expect(await element.name, 'input');
+      test('activeElement', () {
+        var element = driver.activeElement;
+        expect(element.name, 'body');
+        (driver.findElement(const By.cssSelector('input[type=text]'))).click();
+        element = driver.activeElement;
+        expect(element.name, 'input');
       });
 
-      test('windows', () async {
-        var windows = await driver.windows.toList();
+      test('windows', () {
+        var windows = driver.windows.toList();
         expect(windows, hasLength(isPositive));
         expect(windows, everyElement(new isInstanceOf<Window>()));
       });
 
-      test('execute', () async {
-        WebElement button =
-            await driver.findElement(const By.tagName('button'));
+      test('execute', () {
+        WebElement button = driver.findElement(const By.tagName('button'));
         String script = '''
             arguments[1].textContent = arguments[0];
             return arguments[1];''';
-        var e = await driver.execute(script, ['new text', button]);
-        expect(await e.text, 'new text');
+        var e = driver.execute(script, ['new text', button]);
+        expect(e.text, 'new text');
       });
 
-      test('executeAsync', () async {
-        WebElement button =
-            await driver.findElement(const By.tagName('button'));
+      test('executeAsync', () {
+        WebElement button = driver.findElement(const By.tagName('button'));
         String script = '''
             arguments[1].textContent = arguments[0];
             arguments[2](arguments[1]);''';
-        var e = await driver.executeAsync(script, ['new text', button]);
-        expect(await e.text, 'new text');
+        var e = driver.executeAsync(script, ['new text', button]);
+        expect(e.text, 'new text');
       });
 
-      test('captureScreenshot', () async {
-        var screenshot = await driver.captureScreenshot().toList();
+      test('captureScreenshot', () {
+        var screenshot = driver.captureScreenshotAsList().toList();
         expect(screenshot, hasLength(isPositive));
         expect(screenshot, everyElement(new isInstanceOf<int>()));
       });
 
-      test('captureScreenshotAsList', () async {
-        var screenshot = await driver.captureScreenshotAsList();
+      test('captureScreenshotAsList', () {
+        var screenshot = driver.captureScreenshotAsList();
         expect(screenshot, hasLength(isPositive));
         expect(screenshot, everyElement(new isInstanceOf<int>()));
       });
 
-      test('captureScreenshotAsBase64', () async {
-        var screenshot = await driver.captureScreenshotAsBase64();
+      test('captureScreenshotAsBase64', () {
+        var screenshot = driver.captureScreenshotAsBase64();
         expect(screenshot, hasLength(isPositive));
         expect(screenshot, new isInstanceOf<String>());
       });
 
-      test('future based event listeners work with script timeouts', () async {
-        driver.addEventListener((WebDriverCommandEvent e) async {
-          return await new Future.delayed(
-              new Duration(milliseconds: 1000), (() {}));
-        });
-
+      test('event listeners work with script timeouts', () {
         try {
           driver.timeouts.setScriptTimeout(new Duration(seconds: 1));
-          await driver.executeAsync('', []);
+          driver.executeAsync('', []);
           fail('Did not throw timeout as expected');
         } catch (e) {
           expect(e.toString(), contains('asynchronous script timeout'));
         }
       });
 
-      test('future based event listeners ordered appropriately', () async {
+      test('event listeners ordered appropriately', () {
         var eventList = new List<int>();
-        int millisDelay = 2000;
         int current = 0;
-        driver.addEventListener((WebDriverCommandEvent e) async {
-          return await new Future.delayed(
-              new Duration(milliseconds: millisDelay), (() {
-            eventList.add(current++);
-            millisDelay = (millisDelay / 2).round();
-          }));
+        driver.addEventListener((WebDriverCommandEvent e) {
+          eventList.add(current++);
         });
 
         for (int i = 0; i < 10; i++) {
-          await driver.title; // GET request.
+          driver.title; // GET request.
         }
         expect(eventList, hasLength(10));
         for (int i = 0; i < 10; i++) {
@@ -218,5 +195,5 @@
         }
       });
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/web_element_test.dart b/test/web_element_test.dart
index 4127a06..bfb26d0 100644
--- a/test/web_element_test.dart
+++ b/test/web_element_test.dart
@@ -16,10 +16,9 @@
 library webdriver.web_element_test;
 
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
-import 'test_util.dart';
+import 'sync_io_config.dart' as config;
 
 void main() {
   group('WebElement', () {
@@ -32,162 +31,159 @@
     WebElement disabled;
     WebElement invisible;
 
-    setUp(() async {
-      driver = await config.createTestDriver();
-      await driver.get(config.testPagePath);
-      table = await driver.findElement(const By.tagName('table'));
-      button = await driver.findElement(const By.tagName('button'));
-      form = await driver.findElement(const By.tagName('form'));
-      textInput =
-          await driver.findElement(const By.cssSelector('input[type=text]'));
-      checkbox = await driver
-          .findElement(const By.cssSelector('input[type=checkbox]'));
-      disabled = await driver
-          .findElement(const By.cssSelector('input[type=password]'));
-      invisible = await driver.findElement(const By.tagName('div'));
+    setUp(() {
+      driver = config.createTestDriver();
+      driver.get(config.testPagePath);
+      table = driver.findElement(const By.tagName('table'));
+      button = driver.findElement(const By.tagName('button'));
+      form = driver.findElement(const By.tagName('form'));
+      textInput = driver.findElement(const By.cssSelector('input[type=text]'));
+      checkbox =
+          driver.findElement(const By.cssSelector('input[type=checkbox]'));
+      disabled =
+          driver.findElement(const By.cssSelector('input[type=password]'));
+      invisible = driver.findElement(const By.tagName('div'));
     });
 
-    tearDown(() async {
+    tearDown(() {
       if (driver != null) {
-        await driver.quit();
+        driver.quit();
       }
       driver = null;
     });
 
-    test('click', () async {
-      await button.click();
-      var alert = await driver.switchTo.alert;
-      await alert.accept();
+    test('click', () {
+      button.click();
+      var alert = driver.switchTo.alert;
+      alert.accept();
     });
 
-    test('submit', () async {
-      await form.submit();
-      var alert = await driver.switchTo.alert;
+    test('submit', () {
+      form.submit();
+      var alert = driver.switchTo.alert;
       expect(alert.text, 'form submitted');
-      await alert.accept();
+      alert.accept();
     });
 
-    test('sendKeys', () async {
-      await textInput.sendKeys('some keys');
-      expect(await textInput.attributes['value'], 'some keys');
+    test('sendKeys', () {
+      textInput.sendKeys('some keys');
+      expect(textInput.attributes['value'], 'some keys');
     });
 
-    test('clear', () async {
-      await textInput.sendKeys('some keys');
-      await textInput.clear();
-      expect(await textInput.attributes['value'], '');
+    test('clear', () {
+      textInput.sendKeys('some keys');
+      textInput.clear();
+      expect(textInput.attributes['value'], '');
     });
 
-    test('enabled', () async {
-      expect(await table.enabled, isTrue);
-      expect(await button.enabled, isTrue);
-      expect(await form.enabled, isTrue);
-      expect(await textInput.enabled, isTrue);
-      expect(await checkbox.enabled, isTrue);
-      expect(await disabled.enabled, isFalse);
+    test('enabled', () {
+      expect(table.enabled, isTrue);
+      expect(button.enabled, isTrue);
+      expect(form.enabled, isTrue);
+      expect(textInput.enabled, isTrue);
+      expect(checkbox.enabled, isTrue);
+      expect(disabled.enabled, isFalse);
     });
 
-    test('displayed', () async {
-      expect(await table.displayed, isTrue);
-      expect(await button.displayed, isTrue);
-      expect(await form.displayed, isTrue);
-      expect(await textInput.displayed, isTrue);
-      expect(await checkbox.displayed, isTrue);
-      expect(await disabled.displayed, isTrue);
-      expect(await invisible.displayed, isFalse);
+    test('displayed', () {
+      expect(table.displayed, isTrue);
+      expect(button.displayed, isTrue);
+      expect(form.displayed, isTrue);
+      expect(textInput.displayed, isTrue);
+      expect(checkbox.displayed, isTrue);
+      expect(disabled.displayed, isTrue);
+      expect(invisible.displayed, isFalse);
     });
 
-    test('location -- table', () async {
-      var location = await table.location;
-      expect(location, isPoint);
+    test('location -- table', () {
+      var location = table.location;
+      expect(location, config.isPoint);
       expect(location.x, isNonNegative);
       expect(location.y, isNonNegative);
     });
 
-    test('location -- invisible', () async {
-      var location = await invisible.location;
-      expect(location, isPoint);
+    test('location -- invisible', () {
+      var location = invisible.location;
+      expect(location, config.isPoint);
       expect(location.x, 0);
       expect(location.y, 0);
     });
 
-    test('size -- table', () async {
-      var size = await table.size;
-      expect(size, isRectangle);
+    test('size -- table', () {
+      var size = table.size;
+      expect(size, config.isRectangle);
       expect(size.width, isNonNegative);
       expect(size.height, isNonNegative);
     });
 
-    test('size -- invisible', () async {
-      var size = await invisible.size;
-      expect(size, isRectangle);
+    test('size -- invisible', () {
+      var size = invisible.size;
+      expect(size, config.isRectangle);
       // TODO(DrMarcII): I thought these should be 0
       expect(size.width, isNonNegative);
       expect(size.height, isNonNegative);
     });
 
-    test('name', () async {
-      expect(await table.name, 'table');
-      expect(await button.name, 'button');
-      expect(await form.name, 'form');
-      expect(await textInput.name, 'input');
+    test('name', () {
+      expect(table.name, 'table');
+      expect(button.name, 'button');
+      expect(form.name, 'form');
+      expect(textInput.name, 'input');
     });
 
-    test('text', () async {
-      expect(await table.text, 'r1c1 r1c2\nr2c1 r2c2');
-      expect(await button.text, 'button');
-      expect(await invisible.text, '');
+    test('text', () {
+      expect(table.text, 'r1c1 r1c2\nr2c1 r2c2');
+      expect(button.text, 'button');
+      expect(invisible.text, '');
     });
 
-    test('findElement -- success', () async {
-      var element = await table.findElement(const By.tagName('tr'));
-      expect(element, isWebElement);
+    test('findElement -- success', () {
+      var element = table.findElement(const By.tagName('tr'));
+      expect(element, config.isSyncWebElement);
     });
 
-    test('findElement -- failure', () async {
+    test('findElement -- failure', () {
       try {
-        await button.findElement(const By.tagName('tr'));
+        button.findElement(const By.tagName('tr'));
         throw 'Expected NoSuchElementException';
       } on NoSuchElementException {}
     });
 
-    test('findElements -- 1 found', () async {
-      var elements = await form
-          .findElements(const By.cssSelector('input[type=text]'))
-          .toList();
+    test('findElements -- 1 found', () {
+      var elements =
+          form.findElements(const By.cssSelector('input[type=text]')).toList();
       expect(elements, hasLength(1));
-      expect(elements, everyElement(isWebElement));
+      expect(elements, everyElement(config.isSyncWebElement));
     });
 
-    test('findElements -- 4 found', () async {
-      var elements = await table.findElements(const By.tagName('td')).toList();
+    test('findElements -- 4 found', () {
+      var elements = table.findElements(const By.tagName('td')).toList();
       expect(elements, hasLength(4));
-      expect(elements, everyElement(isWebElement));
+      expect(elements, everyElement(config.isSyncWebElement));
     });
 
-    test('findElements -- 0 found', () async {
-      var elements = await form.findElements(const By.tagName('td')).toList();
+    test('findElements -- 0 found', () {
+      var elements = form.findElements(const By.tagName('td')).toList();
       expect(elements, isEmpty);
     });
 
-    test('attributes', () async {
-      expect(await table.attributes['id'], 'table1');
-      expect(await table.attributes['non-standard'], 'a non standard attr');
-      expect(await table.attributes['disabled'], isNull);
-      expect(await disabled.attributes['disabled'], 'true');
+    test('attributes', () {
+      expect(table.attributes['id'], 'table1');
+      expect(table.attributes['non-standard'], 'a non standard attr');
+      expect(table.attributes['disabled'], isNull);
+      expect(disabled.attributes['disabled'], 'true');
     });
 
-    test('cssProperties', () async {
-      expect(await invisible.cssProperties['display'], 'none');
-      expect(await invisible.cssProperties['background-color'],
-          'rgba(255, 0, 0, 1)');
-      expect(await invisible.cssProperties['direction'], 'ltr');
+    test('cssProperties', () {
+      expect(invisible.cssProperties['display'], 'none');
+      expect(invisible.cssProperties['background-color'], 'rgba(255, 0, 0, 1)');
+      expect(invisible.cssProperties['direction'], 'ltr');
     });
 
-    test('equals', () async {
-      expect(await invisible.equals(disabled), isFalse);
-      var element = await driver.findElement(const By.cssSelector('table'));
-      expect(await element.equals(table), isTrue);
+    test('equals', () {
+      expect(invisible.equals(disabled), isFalse);
+      var element = driver.findElement(const By.cssSelector('table'));
+      expect(element.equals(table), isTrue);
     });
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/test/window_test.dart b/test/window_test.dart
index 66aae90..3353748 100644
--- a/test/window_test.dart
+++ b/test/window_test.dart
@@ -18,58 +18,53 @@
 import 'dart:math' show Point, Rectangle;
 
 import 'package:test/test.dart';
-import 'package:webdriver/core.dart';
-import 'package:webdriver/support/async.dart';
+import 'package:webdriver/sync_core.dart';
 
-import 'io_config.dart' as config;
+import 'sync_io_config.dart' as config;
 
 void main() {
   group('Window', () {
     WebDriver driver;
 
-    setUp(() async {
-      driver = await config.createTestDriver();
+    setUp(() {
+      driver = config.createTestDriver();
     });
 
-    tearDown(() async {
+    tearDown(() {
       if (driver != null) {
-        await driver.quit();
+        driver.quit();
       }
       driver = null;
     });
 
-    test('size', () async {
-      var window = await driver.window;
+    test('size', () {
+      var window = driver.window;
       var size = const Rectangle<int>(0, 0, 600, 400);
-      await window.setSize(size);
-      expect(await window.size, size);
+      window.setSize(size);
+      expect(window.size, size);
     });
 
-    test('location', () async {
-      var window = await driver.window;
+    test('location', () {
+      var window = driver.window;
       var position = const Point<int>(100, 200);
-      await window.setLocation(position);
-      expect(await window.location, position);
+      window.setLocation(position);
+      expect(window.location, position);
     }, skip: 'unreliable');
 
     // May not work on some OS/browser combinations (notably Mac OS X).
-    test('maximize', () async {
-      var window = await driver.window;
-      await window.setSize(const Rectangle<int>(0, 0, 300, 200));
-      await window.setLocation(const Point<int>(100, 200));
-      await window.maximize();
+    test('maximize', () {
+      var window = driver.window;
+      window.setSize(const Rectangle<int>(0, 0, 300, 200));
+      window.setLocation(const Point<int>(100, 200));
+      window.maximize();
 
-      // maximizing can take some time
-      await waitFor(() async => (await window.size).height,
-          matcher: greaterThan(200));
-
-      var location = await window.location;
-      var size = await window.size;
+      var location = window.location;
+      var size = window.size;
       // Changed from `lessThan(100)` to pass the test on Mac.
       expect(location.x, lessThanOrEqualTo(100));
       expect(location.y, lessThan(200));
       expect(size.height, greaterThan(200));
       expect(size.width, greaterThan(300));
     }, skip: 'unreliable');
-  });
+  }, timeout: new Timeout(new Duration(minutes: 1)));
 }
diff --git a/tool/travis.sh b/tool/travis.sh
index bddce06..ebd46dd 100755
--- a/tool/travis.sh
+++ b/tool/travis.sh
@@ -28,7 +28,7 @@
 PID=$!
 
 # Run tests.
-pub run test -r expanded -p vm,content-shell -j 1
+pub run test -r expanded -p vm -j 1
 TEST_STATUS=$?
 if [[ $TEST_STATUS -ne 0 ]]; then
   STATUS=$TEST_STATUS