Adds support W3C spec exception handling. (#171)

* Refactor exception to allow JSON and W3C spec versions. Adds JSON version.

* Adds exception type of W3C spec.

* Extends w3c web element test to account for exception handling.

* Dartfmt

* Address pull request comments.
diff --git a/lib/src/sync/exception.dart b/lib/src/sync/exception.dart
index 14b3688..a881820 100644
--- a/lib/src/sync/exception.dart
+++ b/lib/src/sync/exception.dart
@@ -14,208 +14,10 @@
 
 library webdriver.exception;
 
+/// Exceptions returned from WebDriver implementation.
+///
+/// Exception handling differs heavily based on protocol.
 abstract class WebDriverException implements Exception {
-  /// Either the status value returned in the JSON response (preferred) or the
-  /// HTTP status code.
-  final int statusCode;
-
   /// A message describing the error.
-  final String message;
-
-  factory WebDriverException(
-      {int httpStatusCode, String httpReasonPhrase, dynamic jsonResp}) {
-    if (jsonResp is Map) {
-      final status = jsonResp['status'];
-      final message = jsonResp['value']['message'];
-
-      switch (status) {
-        case 0:
-          throw new StateError(
-              'Not a WebDriverError Status: 0 Message: $message');
-        case 6: // NoSuchDriver
-          return new NoSuchDriverException(status, message);
-        case 7: // NoSuchElement
-          return new NoSuchElementException(status, message);
-        case 8: // NoSuchFrame
-          return new NoSuchFrameException(status, message);
-        case 9: // UnknownCommand
-          return new UnknownCommandException(status, message);
-        case 10: // StaleElementReferenceException
-          return new StaleElementReferenceException(status, message);
-        case 11: // ElementNotVisible
-          return new ElementNotVisibleException(status, message);
-        case 12: // InvalidElementState
-          return new InvalidElementStateException(status, message);
-        case 15: // ElementIsNotSelectable
-          return new ElementIsNotSelectableException(status, message);
-        case 17: // JavaScriptError
-          return new JavaScriptException(status, message);
-        case 19: // XPathLookupError
-          return new XPathLookupException(status, message);
-        case 21: // Timeout
-          return new TimeoutException(status, message);
-        case 23: // NoSuchWindow
-          return new NoSuchWindowException(status, message);
-        case 24: // InvalidCookieDomain
-          return new InvalidCookieDomainException(status, message);
-        case 25: // UnableToSetCookie
-          return new UnableToSetCookieException(status, message);
-        case 26: // UnexpectedAlertOpen
-          return new UnexpectedAlertOpenException(status, message);
-        case 27: // NoSuchAlert
-          return new NoSuchAlertException(status, message);
-        case 29: // InvalidElementCoordinates
-          return new InvalidElementCoordinatesException(status, message);
-        case 30: // IMENotAvailable
-          return new IMENotAvailableException(status, message);
-        case 31: // IMEEngineActivationFailed
-          return new IMEEngineActivationFailedException(status, message);
-        case 32: // InvalidSelector
-          return new InvalidSelectorException(status, message);
-        case 33: // SessionNotCreatedException
-          return new SessionNotCreatedException(status, message);
-        case 34: // MoveTargetOutOfBounds
-          return new MoveTargetOutOfBoundsException(status, message);
-        case 13: // UnknownError
-        default: // new error?
-          return new UnknownException(status, message);
-      }
-    }
-    if (jsonResp != null) {
-      return new InvalidRequestException(httpStatusCode, jsonResp);
-    }
-    return new InvalidRequestException(httpStatusCode, httpReasonPhrase);
-  }
-
-  const WebDriverException._(this.statusCode, this.message);
-
-  @override
-  String toString() => '$runtimeType ($statusCode): $message';
-
-  @override
-  bool operator ==(other) =>
-      other != null &&
-      other.runtimeType == this.runtimeType &&
-      other.statusCode == this.statusCode &&
-      other.message == this.message;
-
-  @override
-  int get hashCode => statusCode + message.hashCode;
-}
-
-class InvalidRequestException extends WebDriverException {
-  const InvalidRequestException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class UnknownException extends WebDriverException {
-  const UnknownException(statusCode, message) : super._(statusCode, message);
-}
-
-class NoSuchDriverException extends WebDriverException {
-  const NoSuchDriverException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class NoSuchElementException extends WebDriverException {
-  const NoSuchElementException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class NoSuchFrameException extends WebDriverException {
-  const NoSuchFrameException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class UnknownCommandException extends WebDriverException {
-  const UnknownCommandException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class StaleElementReferenceException extends WebDriverException {
-  const StaleElementReferenceException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class ElementNotVisibleException extends WebDriverException {
-  const ElementNotVisibleException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class InvalidElementStateException extends WebDriverException {
-  const InvalidElementStateException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class ElementIsNotSelectableException extends WebDriverException {
-  const ElementIsNotSelectableException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class JavaScriptException extends WebDriverException {
-  const JavaScriptException(statusCode, message) : super._(statusCode, message);
-}
-
-class XPathLookupException extends WebDriverException {
-  const XPathLookupException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class TimeoutException extends WebDriverException {
-  const TimeoutException(statusCode, message) : super._(statusCode, message);
-}
-
-class NoSuchWindowException extends WebDriverException {
-  const NoSuchWindowException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class InvalidCookieDomainException extends WebDriverException {
-  const InvalidCookieDomainException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class UnableToSetCookieException extends WebDriverException {
-  const UnableToSetCookieException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class UnexpectedAlertOpenException extends WebDriverException {
-  const UnexpectedAlertOpenException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class NoSuchAlertException extends WebDriverException {
-  const NoSuchAlertException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class InvalidElementCoordinatesException extends WebDriverException {
-  const InvalidElementCoordinatesException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class IMENotAvailableException extends WebDriverException {
-  const IMENotAvailableException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class IMEEngineActivationFailedException extends WebDriverException {
-  const IMEEngineActivationFailedException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class InvalidSelectorException extends WebDriverException {
-  const InvalidSelectorException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class SessionNotCreatedException extends WebDriverException {
-  const SessionNotCreatedException(statusCode, message)
-      : super._(statusCode, message);
-}
-
-class MoveTargetOutOfBoundsException extends WebDriverException {
-  const MoveTargetOutOfBoundsException(statusCode, message)
-      : super._(statusCode, message);
+  String get message;
 }
diff --git a/lib/src/sync/json_wire_spec/exception.dart b/lib/src/sync/json_wire_spec/exception.dart
new file mode 100644
index 0000000..0c0b035
--- /dev/null
+++ b/lib/src/sync/json_wire_spec/exception.dart
@@ -0,0 +1,226 @@
+// 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.exception;
+
+import '../exception.dart';
+
+abstract class JsonWireWebDriverException implements WebDriverException {
+  /// The status value returned in the JSON response (preferred) or the
+  /// HTTP status code.
+  final int statusCode;
+
+  final String _message;
+
+  /// A message describing the error.
+  @override
+  String get message => _message;
+
+  factory JsonWireWebDriverException(
+      {int httpStatusCode, String httpReasonPhrase, dynamic jsonResp}) {
+    if (jsonResp is Map) {
+      final status = jsonResp['status'];
+      final message = jsonResp['value']['message'];
+
+      switch (status) {
+        case 0:
+          throw new StateError(
+              'Not a WebDriverError Status: 0 Message: $message');
+        case 6: // NoSuchDriver
+          return new NoSuchDriverException(status, message);
+        case 7: // NoSuchElement
+          return new NoSuchElementException(status, message);
+        case 8: // NoSuchFrame
+          return new NoSuchFrameException(status, message);
+        case 9: // UnknownCommand
+          return new UnknownCommandException(status, message);
+        case 10: // StaleElementReferenceException
+          return new StaleElementReferenceException(status, message);
+        case 11: // ElementNotVisible
+          return new ElementNotVisibleException(status, message);
+        case 12: // InvalidElementState
+          return new InvalidElementStateException(status, message);
+        case 15: // ElementIsNotSelectable
+          return new ElementIsNotSelectableException(status, message);
+        case 17: // JavaScriptError
+          return new JavaScriptException(status, message);
+        case 19: // XPathLookupError
+          return new XPathLookupException(status, message);
+        case 21: // Timeout
+          return new TimeoutException(status, message);
+        case 23: // NoSuchWindow
+          return new NoSuchWindowException(status, message);
+        case 24: // InvalidCookieDomain
+          return new InvalidCookieDomainException(status, message);
+        case 25: // UnableToSetCookie
+          return new UnableToSetCookieException(status, message);
+        case 26: // UnexpectedAlertOpen
+          return new UnexpectedAlertOpenException(status, message);
+        case 27: // NoSuchAlert
+          return new NoSuchAlertException(status, message);
+        case 29: // InvalidElementCoordinates
+          return new InvalidElementCoordinatesException(status, message);
+        case 30: // IMENotAvailable
+          return new IMENotAvailableException(status, message);
+        case 31: // IMEEngineActivationFailed
+          return new IMEEngineActivationFailedException(status, message);
+        case 32: // InvalidSelector
+          return new InvalidSelectorException(status, message);
+        case 33: // SessionNotCreatedException
+          return new SessionNotCreatedException(status, message);
+        case 34: // MoveTargetOutOfBounds
+          return new MoveTargetOutOfBoundsException(status, message);
+        case 13: // UnknownError
+        default: // new error?
+          return new UnknownException(status, message);
+      }
+    }
+    if (jsonResp != null) {
+      return new InvalidRequestException(httpStatusCode, jsonResp);
+    }
+    return new InvalidRequestException(httpStatusCode, httpReasonPhrase);
+  }
+
+  const JsonWireWebDriverException._(this.statusCode, this._message);
+
+  @override
+  String toString() => '$runtimeType ($statusCode): $message';
+
+  @override
+  bool operator ==(other) =>
+      other != null &&
+      other.runtimeType == this.runtimeType &&
+      other.statusCode == this.statusCode &&
+      other.message == this.message;
+
+  @override
+  int get hashCode => statusCode + message.hashCode;
+}
+
+class InvalidRequestException extends JsonWireWebDriverException {
+  const InvalidRequestException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class UnknownException extends JsonWireWebDriverException {
+  const UnknownException(statusCode, message) : super._(statusCode, message);
+}
+
+class NoSuchDriverException extends JsonWireWebDriverException {
+  const NoSuchDriverException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class NoSuchElementException extends JsonWireWebDriverException {
+  const NoSuchElementException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class NoSuchFrameException extends JsonWireWebDriverException {
+  const NoSuchFrameException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class UnknownCommandException extends JsonWireWebDriverException {
+  const UnknownCommandException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class StaleElementReferenceException extends JsonWireWebDriverException {
+  const StaleElementReferenceException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class ElementNotVisibleException extends JsonWireWebDriverException {
+  const ElementNotVisibleException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class InvalidElementStateException extends JsonWireWebDriverException {
+  const InvalidElementStateException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class ElementIsNotSelectableException extends JsonWireWebDriverException {
+  const ElementIsNotSelectableException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class JavaScriptException extends JsonWireWebDriverException {
+  const JavaScriptException(statusCode, message) : super._(statusCode, message);
+}
+
+class XPathLookupException extends JsonWireWebDriverException {
+  const XPathLookupException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class TimeoutException extends JsonWireWebDriverException {
+  const TimeoutException(statusCode, message) : super._(statusCode, message);
+}
+
+class NoSuchWindowException extends JsonWireWebDriverException {
+  const NoSuchWindowException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class InvalidCookieDomainException extends JsonWireWebDriverException {
+  const InvalidCookieDomainException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class UnableToSetCookieException extends JsonWireWebDriverException {
+  const UnableToSetCookieException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class UnexpectedAlertOpenException extends JsonWireWebDriverException {
+  const UnexpectedAlertOpenException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class NoSuchAlertException extends JsonWireWebDriverException {
+  const NoSuchAlertException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class InvalidElementCoordinatesException extends JsonWireWebDriverException {
+  const InvalidElementCoordinatesException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class IMENotAvailableException extends JsonWireWebDriverException {
+  const IMENotAvailableException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class IMEEngineActivationFailedException extends JsonWireWebDriverException {
+  const IMEEngineActivationFailedException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class InvalidSelectorException extends JsonWireWebDriverException {
+  const InvalidSelectorException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class SessionNotCreatedException extends JsonWireWebDriverException {
+  const SessionNotCreatedException(statusCode, message)
+      : super._(statusCode, message);
+}
+
+class MoveTargetOutOfBoundsException extends JsonWireWebDriverException {
+  const MoveTargetOutOfBoundsException(statusCode, message)
+      : super._(statusCode, message);
+}
diff --git a/lib/src/sync/json_wire_spec/response_processor.dart b/lib/src/sync/json_wire_spec/response_processor.dart
index 212df91..8a2d9ec 100644
--- a/lib/src/sync/json_wire_spec/response_processor.dart
+++ b/lib/src/sync/json_wire_spec/response_processor.dart
@@ -16,7 +16,7 @@
 
 import 'package:sync_http/sync_http.dart';
 
-import '../exception.dart' show WebDriverException;
+import 'exception.dart' show JsonWireWebDriverException;
 
 /// Handles responses from the JSON wire protocol.
 dynamic processJsonWireResponse(SyncHttpClientResponse response, bool value) {
@@ -30,7 +30,7 @@
       (responseBody is Map &&
           responseBody['status'] != null &&
           responseBody['status'] != 0)) {
-    throw new WebDriverException(
+    throw new JsonWireWebDriverException(
         httpStatusCode: response.statusCode,
         httpReasonPhrase: response.reasonPhrase,
         jsonResp: responseBody);
diff --git a/lib/src/sync/w3c_spec/exception.dart b/lib/src/sync/w3c_spec/exception.dart
new file mode 100644
index 0000000..8fee62c
--- /dev/null
+++ b/lib/src/sync/w3c_spec/exception.dart
@@ -0,0 +1,70 @@
+// 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.exception;
+
+import '../exception.dart';
+
+class W3cWebDriverException implements WebDriverException {
+  /// The HTTP status code.
+  final int httpStatusCode;
+
+  final String _message;
+
+  /// Error type.
+  final String error;
+
+  /// Stacktrace
+  final String stackTrace;
+
+  /// A message describing the error.
+  @override
+  String get message => _message;
+
+  factory W3cWebDriverException({int httpStatusCode, dynamic jsonResp}) {
+    if (jsonResp is Map && jsonResp.keys.contains('value')) {
+      final value = jsonResp['value'];
+      final error = value['error'];
+      final message = value['message'];
+      final stacktrace = value['stacktrace'];
+
+      return new W3cWebDriverException._(
+          httpStatusCode, error, message, stacktrace);
+    }
+    return new InvalidResponseW3cWebDriverException(httpStatusCode);
+  }
+
+  const W3cWebDriverException._(
+      this.httpStatusCode, this.error, this._message, this.stackTrace);
+
+  @override
+  String toString() => '$runtimeType ($httpStatusCode): $message';
+
+  @override
+  bool operator ==(other) =>
+      other != null &&
+      other.runtimeType == this.runtimeType &&
+      other.statusCode == this.httpStatusCode &&
+      other.message == this.message &&
+      other.error == this.error;
+
+  @override
+  int get hashCode => httpStatusCode + message.hashCode;
+}
+
+/// Thrown in case of invalid responses.
+class InvalidResponseW3cWebDriverException extends W3cWebDriverException {
+  const InvalidResponseW3cWebDriverException(int httpStatusCode)
+      : super._(httpStatusCode, null, null, null);
+}
diff --git a/lib/src/sync/w3c_spec/response_processor.dart b/lib/src/sync/w3c_spec/response_processor.dart
index d8e3d8d..a4620f8 100644
--- a/lib/src/sync/w3c_spec/response_processor.dart
+++ b/lib/src/sync/w3c_spec/response_processor.dart
@@ -16,7 +16,7 @@
 
 import 'package:sync_http/sync_http.dart';
 
-import '../exception.dart' show WebDriverException;
+import 'exception.dart' show W3cWebDriverException;
 
 /// Handles responses from the W3C protocol.
 dynamic processW3cResponse(SyncHttpClientResponse response, bool value) {
@@ -26,10 +26,8 @@
   } catch (e) {}
 
   if (response.statusCode < 200 || response.statusCode > 299) {
-    throw new WebDriverException(
-        httpStatusCode: response.statusCode,
-        httpReasonPhrase: response.reasonPhrase,
-        jsonResp: responseBody);
+    throw new W3cWebDriverException(
+        httpStatusCode: response.statusCode, jsonResp: responseBody);
   }
   if (value && responseBody is Map) {
     return responseBody['value'];
diff --git a/test/sync/w3c_web_element.dart b/test/sync/w3c_web_element.dart
index 5157063..a89dfc0 100644
--- a/test/sync/w3c_web_element.dart
+++ b/test/sync/w3c_web_element.dart
@@ -19,6 +19,7 @@
 
 import 'package:test/test.dart';
 import 'package:webdriver/sync_core.dart';
+import 'package:webdriver/src/sync/w3c_spec/exception.dart';
 
 import 'sync_io_config.dart' as config;
 
@@ -133,7 +134,13 @@
       try {
         button.findElement(const By.tagName('tr'));
         throw 'Expected Exception';
-      } on Exception {}
+      } catch (e) {
+        expect(e, new isInstanceOf<W3cWebDriverException>());
+        expect((e as W3cWebDriverException).httpStatusCode, 404);
+        expect((e as W3cWebDriverException).error, 'no such element');
+        expect((e as W3cWebDriverException).message,
+            contains('Unable to locate element'));
+      }
     });
 
     test('findElements -- 1 found', () {
diff --git a/test/sync/web_element.dart b/test/sync/web_element.dart
index 5b7899f..b3b729d 100644
--- a/test/sync/web_element.dart
+++ b/test/sync/web_element.dart
@@ -16,6 +16,7 @@
 library webdriver.web_element_test;
 
 import 'package:test/test.dart';
+import 'package:webdriver/src/sync/json_wire_spec/exception.dart';
 import 'package:webdriver/sync_core.dart';
 
 import 'sync_io_config.dart' as config;
@@ -146,7 +147,9 @@
       try {
         button.findElement(const By.tagName('tr'));
         throw 'Expected NoSuchElementException';
-      } on NoSuchElementException {}
+      } catch (e) {
+        expect(e, new isInstanceOf<NoSuchElementException>());
+      }
     });
 
     test('findElements -- 1 found', () {