Add W3C mouse support in Dart webdriver. (#193)

Extracted a common interface and implemented the W3C version via "actions" command.
diff --git a/lib/src/sync/json_wire_spec/mouse.dart b/lib/src/sync/json_wire_spec/mouse.dart
index 4e8d9c5..a5c3bf1 100644
--- a/lib/src/sync/json_wire_spec/mouse.dart
+++ b/lib/src/sync/json_wire_spec/mouse.dart
@@ -15,35 +15,15 @@
 import '../common.dart';
 import '../web_driver.dart';
 import '../web_element.dart';
+import '../mouse.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 {
+class JsonWireMouse extends Mouse {
   final WebDriver _driver;
   final Resolver _resolver;
 
-  Mouse(this._driver) : _resolver = new Resolver(_driver, '');
+  JsonWireMouse(this._driver) : _resolver = new Resolver(_driver, '');
 
-  /// Click any mouse button (at the coordinates set by the last moveTo).
+  @override
   void click([MouseButton button]) {
     final json = {};
     if (button is MouseButton) {
@@ -52,8 +32,7 @@
     _resolver.post('click', json);
   }
 
-  /// Click and hold any mouse button (at the coordinates set by the last
-  /// moveTo command).
+  @override
   void down([MouseButton button]) {
     final json = {};
     if (button is MouseButton) {
@@ -62,7 +41,7 @@
     _resolver.post('buttondown', json);
   }
 
-  /// Releases the mouse button previously held (where the mouse is currently at).
+  @override
   void up([MouseButton button]) {
     final json = {};
     if (button is MouseButton) {
@@ -71,22 +50,12 @@
     _resolver.post('buttonup', json);
   }
 
-  /// Double-clicks at the current mouse coordinates (set by moveTo).
+  @override
   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.
+  @override
   void moveTo({WebElement element, int xOffset, int yOffset}) {
     final json = {};
     if (element is WebElement) {
@@ -106,5 +75,5 @@
   int get hashCode => _driver.hashCode;
 
   @override
-  bool operator ==(other) => other is Mouse && other._driver == _driver;
+  bool operator ==(other) => other is JsonWireMouse && other._driver == _driver;
 }
diff --git a/lib/src/sync/json_wire_spec/web_driver.dart b/lib/src/sync/json_wire_spec/web_driver.dart
index 2ca3682..9355055 100644
--- a/lib/src/sync/json_wire_spec/web_driver.dart
+++ b/lib/src/sync/json_wire_spec/web_driver.dart
@@ -32,6 +32,7 @@
 import '../command_processor.dart';
 import '../common.dart';
 import '../keyboard.dart';
+import '../mouse.dart';
 import '../navigation.dart';
 import '../target_locator.dart';
 import '../timeouts.dart';
@@ -159,7 +160,7 @@
   Keyboard get keyboard => new JsonWireKeyboard(this);
 
   @override
-  Mouse get mouse => new Mouse(this);
+  Mouse get mouse => new JsonWireMouse(this);
 
   @override
   String captureScreenshotAsBase64() => getRequest('screenshot');
diff --git a/lib/src/sync/mouse.dart b/lib/src/sync/mouse.dart
new file mode 100644
index 0000000..b58075e
--- /dev/null
+++ b/lib/src/sync/mouse.dart
@@ -0,0 +1,64 @@
+// 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.
+
+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);
+}
+
+abstract class Mouse {
+  /// Click any mouse button (at the coordinates set by the last moveTo).
+  void click([MouseButton button]);
+
+  /// Click and hold any mouse button (at the coordinates set by the last
+  /// moveTo command).
+  void down([MouseButton button]);
+
+  /// Releases the mouse button previously held (where the mouse is currently at).
+  void up([MouseButton button]);
+
+  /// Double-clicks at the current mouse coordinates (set by moveTo).
+  void 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});
+}
diff --git a/lib/src/sync/w3c_spec/mouse.dart b/lib/src/sync/w3c_spec/mouse.dart
new file mode 100644
index 0000000..796d4f7
--- /dev/null
+++ b/lib/src/sync/w3c_spec/mouse.dart
@@ -0,0 +1,120 @@
+// 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.
+
+import '../common.dart';
+import '../web_driver.dart';
+import '../web_element.dart';
+import '../mouse.dart';
+
+class W3cMouse extends Mouse {
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  W3cMouse(this._driver) : _resolver = new Resolver(_driver, '');
+
+  @override
+  void click([MouseButton button = MouseButton.primary]) {
+    _resolver.post('actions', {
+      'actions': [
+        {
+          'type': 'pointer',
+          'id': 'mouses',
+          'actions': [
+            {'type': 'pointerDown', 'button': button.value},
+            {'type': 'pointerUp', 'button': button.value}
+          ]
+        }
+      ]
+    });
+  }
+
+  @override
+  void down([MouseButton button = MouseButton.primary]) {
+    _resolver.post('actions', {
+      'actions': [
+        {
+          'type': 'pointer',
+          'id': 'mouses',
+          'actions': [
+            {'type': 'pointerDown', 'button': button.value}
+          ]
+        }
+      ]
+    });
+  }
+
+  @override
+  void up([MouseButton button = MouseButton.primary]) {
+    _resolver.post('actions', {
+      'actions': [
+        {
+          'type': 'pointer',
+          'id': 'mouses',
+          'actions': [
+            {'type': 'pointerUp', 'button': button.value}
+          ]
+        }
+      ]
+    });
+  }
+
+  @override
+  void doubleClick() {
+    _resolver.post('actions', {
+      'actions': [
+        {
+          'type': 'pointer',
+          'id': 'mouses',
+          'actions': [
+            {'type': 'pointerDown', 'button': MouseButton.primary.value},
+            {'type': 'pointerUp', 'button': MouseButton.primary.value},
+            {'type': 'pointerDown', 'button': MouseButton.primary.value},
+            {'type': 'pointerUp', 'button': MouseButton.primary.value}
+          ]
+        }
+      ]
+    });
+  }
+
+  @override
+  void moveTo({WebElement element, int xOffset, int yOffset}) {
+    _resolver.post('actions', {
+      'actions': [
+        {
+          'type': 'pointer',
+          'id': 'mouses',
+          'actions': [
+            {
+              'type': 'pointerMove',
+              'origin': element is WebElement
+                  ? {w3cElementStr: element.id}
+                  : 'pointer',
+              'x': xOffset ?? 0,
+              'y': yOffset ?? 0
+            }
+          ]
+        }
+      ]
+    });
+  }
+
+  @override
+  String toString() => '$_driver.mouse';
+
+  @override
+  int get hashCode => _driver.hashCode;
+
+  @override
+  bool operator ==(other) => other is W3cMouse && other._driver == _driver;
+}
diff --git a/lib/src/sync/w3c_spec/web_driver.dart b/lib/src/sync/w3c_spec/web_driver.dart
index 15f08ad..520df30 100644
--- a/lib/src/sync/w3c_spec/web_driver.dart
+++ b/lib/src/sync/w3c_spec/web_driver.dart
@@ -17,6 +17,7 @@
 
 import 'element_finder.dart';
 import 'keyboard.dart';
+import 'mouse.dart';
 import 'navigation.dart';
 import 'target_locator.dart';
 import 'timeouts.dart';
@@ -27,9 +28,9 @@
 
 import '../common_spec/cookies.dart';
 import '../keyboard.dart';
+import '../mouse.dart';
 // We don't implement this, but we need the types to define the API.
 import '../json_wire_spec/logs.dart';
-import '../json_wire_spec/mouse.dart';
 
 import '../command_event.dart';
 import '../command_processor.dart';
@@ -144,7 +145,7 @@
   Keyboard get keyboard => new W3cKeyboard(this);
 
   @override
-  Mouse get mouse => throw 'Unsupported in W3C spec, use Actions instead.';
+  Mouse get mouse => new W3cMouse(this);
 
   @override
   String captureScreenshotAsBase64() => getRequest('screenshot');
diff --git a/lib/src/sync/w3c_spec/web_element.dart b/lib/src/sync/w3c_spec/web_element.dart
index 4903aae..bf3e453 100644
--- a/lib/src/sync/w3c_spec/web_element.dart
+++ b/lib/src/sync/w3c_spec/web_element.dart
@@ -83,12 +83,10 @@
   bool get displayed => this.cssProperties['display'] != 'none';
 
   @override
-  // TODO(staats): better exception.
-  Point get location => throw 'Unsupported by W3C spec, use "rect" instead.';
+  Point get location => rect.topLeft;
 
   @override
-  Rectangle<int> get size =>
-      throw 'Unsupported by W3C spec, use "rect" instead.';
+  Rectangle<int> get size => rect;
 
   @override
   Rectangle<int> get rect {
diff --git a/lib/src/sync/web_driver.dart b/lib/src/sync/web_driver.dart
index d2924e8..39cc095 100644
--- a/lib/src/sync/web_driver.dart
+++ b/lib/src/sync/web_driver.dart
@@ -15,6 +15,7 @@
 import 'command_event.dart';
 import 'common.dart';
 import 'keyboard.dart';
+import 'mouse.dart';
 import 'navigation.dart';
 import 'target_locator.dart';
 import 'timeouts.dart';
@@ -25,7 +26,6 @@
 
 import 'common_spec/cookies.dart';
 
-import 'json_wire_spec/mouse.dart';
 import 'json_wire_spec/logs.dart';
 
 typedef void WebDriverListener(WebDriverCommandEvent event);
diff --git a/lib/src/sync/web_element.dart b/lib/src/sync/web_element.dart
index e36563d..4217326 100644
--- a/lib/src/sync/web_element.dart
+++ b/lib/src/sync/web_element.dart
@@ -65,11 +65,9 @@
   ///
   /// This is assumed to be the upper left corner of the element, but its
   /// implementation is not well defined in the JSON spec.
-  @Deprecated('JSON wire legacy support, emulated for newer browsers')
   Point get location;
 
   /// The size of this element.
-  @Deprecated('JSON wire legacy support, emulated for newer browsers')
   Rectangle<int> get size;
 
   /// The bounds of this element.
diff --git a/lib/sync_core.dart b/lib/sync_core.dart
index 03a396d..268aeae 100644
--- a/lib/sync_core.dart
+++ b/lib/sync_core.dart
@@ -33,11 +33,11 @@
 export 'package:webdriver/src/sync/common.dart';
 export 'package:webdriver/src/sync/common_spec/cookies.dart';
 export 'package:webdriver/src/sync/keyboard.dart';
+export 'package:webdriver/src/sync/mouse.dart';
 export 'package:webdriver/src/sync/navigation.dart';
 export 'package:webdriver/src/sync/exception.dart';
 export 'package:webdriver/src/sync/json_wire_spec/exception.dart';
 export 'package:webdriver/src/sync/json_wire_spec/logs.dart';
-export 'package:webdriver/src/sync/json_wire_spec/mouse.dart';
 export 'package:webdriver/src/sync/timeouts.dart';
 export 'package:webdriver/src/sync/target_locator.dart';
 export 'package:webdriver/src/sync/web_driver.dart';
diff --git a/test/firefox_w3c_mouse_test.dart b/test/firefox_w3c_mouse_test.dart
new file mode 100644
index 0000000..9af411a
--- /dev/null
+++ b/test/firefox_w3c_mouse_test.dart
@@ -0,0 +1,20 @@
+// 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.
+
+import 'sync/mouse.dart';
+import 'sync/sync_io_config.dart' as config;
+
+void main() {
+  runTests(config.createFirefoxTestDriver);
+}
diff --git a/test/sync/w3c_web_element.dart b/test/sync/w3c_web_element.dart
index a4af0f5..c9a6be0 100644
--- a/test/sync/w3c_web_element.dart
+++ b/test/sync/w3c_web_element.dart
@@ -95,23 +95,50 @@
     });
 
     test('rect -- table', () {
-      var location = table.rect;
-      expect(location, config.isRectangle);
-      expect(location.left, isNonNegative);
-      expect(location.top, isNonNegative);
-      expect(location.width, isNonNegative);
-      expect(location.height, isNonNegative);
+      var rect = table.rect;
+      expect(rect, config.isRectangle);
+      expect(rect.left, isNonNegative);
+      expect(rect.top, isNonNegative);
+      expect(rect.width, isNonNegative);
+      expect(rect.height, isNonNegative);
+    });
+
+    test('rect -- invisible', () {
+      var rect = invisible.rect;
+      expect(rect, config.isRectangle);
+      expect(rect.left, 0);
+      expect(rect.top, 0);
+      expect(rect.width, 0);
+      expect(rect.height, 0);
+    });
+
+    test('location -- table', () {
+      var location = table.location;
+      expect(location, config.isPoint);
+      expect(location.x, isNonNegative);
+      expect(location.y, isNonNegative);
     });
 
     test('location -- invisible', () {
-      var location = invisible.rect;
-      expect(location, config.isRectangle);
-      expect(location.left, 0);
-      expect(location.top, 0);
-      expect(location.width, 0);
-      expect(location.height, 0);
+      var location = invisible.location;
+      expect(location, config.isPoint);
+      expect(location.x, 0);
+      expect(location.y, 0);
     });
 
+    test('size -- table', () {
+      var size = table.size;
+      expect(size, config.isRectangle);
+      expect(size.width, isNonNegative);
+      expect(size.height, isNonNegative);
+    });
+
+    test('size -- invisible', () {
+      var size = invisible.size;
+      expect(size, config.isRectangle);
+      expect(size.width, 0);
+      expect(size.height, 0);
+    });
     test('name', () {
       expect(table.name, 'table');
       expect(button.name, 'button');