Add W3C keyboard support in Dart webdriver. (#191)

* Add W3C keyboard support in Dart webdriver.

Extracted a common interface, and implemented it in the W3C version via "actions" command.

Also some minor fixes in W3C webdriver setup.

* Convert all quotes to single.
diff --git a/lib/src/sync/json_wire_spec/keyboard.dart b/lib/src/sync/json_wire_spec/keyboard.dart
index 627bbe6..9240c36 100644
--- a/lib/src/sync/json_wire_spec/keyboard.dart
+++ b/lib/src/sync/json_wire_spec/keyboard.dart
@@ -13,88 +13,31 @@
 // limitations under the License.
 
 import '../common.dart';
+import '../keyboard.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;
-
+class JsonWireKeyboard extends Keyboard {
   final WebDriver _driver;
   final Resolver _resolver;
 
-  Keyboard(this._driver) : _resolver = new Resolver(_driver, '');
+  JsonWireKeyboard(this._driver) : _resolver = new Resolver(_driver, '');
 
-  /// Simulate pressing many keys at once as a 'chord'.
+  @override
   void sendChord(Iterable<String> chordToSend) {
-    sendKeys(createChord(chordToSend));
+    sendKeys(_createChord(chordToSend));
   }
 
-  /// Creates a string representation of a chord suitable for use in WebDriver.
-  String createChord(Iterable<String> chord) {
+  String _createChord(Iterable<String> chord) {
     StringBuffer chordString = new StringBuffer();
     for (String s in chord) {
       chordString.write(s);
     }
-    chordString.write(nullChar);
+    chordString.write(Keyboard.nullChar);
 
     return chordString.toString();
   }
 
-  /// Send [keysToSend] to the active element.
+  @override
   void sendKeys(String keysToSend) {
     _resolver.post('keys', {
       'value': [keysToSend]
@@ -108,5 +51,6 @@
   int get hashCode => _driver.hashCode;
 
   @override
-  bool operator ==(other) => other is Keyboard && other._driver == _driver;
+  bool operator ==(other) =>
+      other is JsonWireKeyboard && 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 9c4a55a..2ca3682 100644
--- a/lib/src/sync/json_wire_spec/web_driver.dart
+++ b/lib/src/sync/json_wire_spec/web_driver.dart
@@ -31,6 +31,7 @@
 import '../command_event.dart';
 import '../command_processor.dart';
 import '../common.dart';
+import '../keyboard.dart';
 import '../navigation.dart';
 import '../target_locator.dart';
 import '../timeouts.dart';
@@ -155,7 +156,7 @@
   Timeouts get timeouts => new JsonWireTimeouts(this);
 
   @override
-  Keyboard get keyboard => new Keyboard(this);
+  Keyboard get keyboard => new JsonWireKeyboard(this);
 
   @override
   Mouse get mouse => new Mouse(this);
diff --git a/lib/src/sync/keyboard.dart b/lib/src/sync/keyboard.dart
new file mode 100644
index 0000000..3bd3135
--- /dev/null
+++ b/lib/src/sync/keyboard.dart
@@ -0,0 +1,78 @@
+// 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.
+
+abstract 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;
+
+  /// Simulate pressing many keys at once as a 'chord'.
+  void sendChord(Iterable<String> chordToSend);
+
+  /// Send [keysToSend] to the active element.
+  void sendKeys(String keysToSend);
+}
diff --git a/lib/src/sync/w3c_spec/keyboard.dart b/lib/src/sync/w3c_spec/keyboard.dart
new file mode 100644
index 0000000..7a4bd05
--- /dev/null
+++ b/lib/src/sync/w3c_spec/keyboard.dart
@@ -0,0 +1,67 @@
+// 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 '../keyboard.dart';
+import '../web_driver.dart';
+
+class W3cKeyboard extends Keyboard {
+  final WebDriver _driver;
+  final Resolver _resolver;
+
+  W3cKeyboard(this._driver) : _resolver = new Resolver(_driver, '');
+
+  @override
+  void sendChord(Iterable<String> chordToSend) {
+    final keyDownActions = <Map<String, String>>[];
+    final keyUpActions = <Map<String, String>>[];
+    for (String s in chordToSend) {
+      keyDownActions.add({'type': 'keyDown', 'value': s});
+      keyUpActions.add({'type': 'keyUp', 'value': s});
+    }
+    _resolver.post('actions', {
+      'actions': [
+        {
+          'type': 'key',
+          'id': 'keys',
+          'actions':
+              keyDownActions + keyUpActions.reversed.toList(growable: false)
+        }
+      ]
+    });
+  }
+
+  @override
+  void sendKeys(String keysToSend) {
+    final keyActions = <Map<String, String>>[];
+    for (int i = 0; i < keysToSend.length; ++i) {
+      keyActions.add({'type': 'keyDown', 'value': keysToSend[i]});
+      keyActions.add({'type': 'keyUp', 'value': keysToSend[i]});
+    }
+    _resolver.post('actions', {
+      'actions': [
+        {'type': 'key', 'id': 'keys', 'actions': keyActions}
+      ]
+    });
+  }
+
+  @override
+  String toString() => '$_driver.keyboard';
+
+  @override
+  int get hashCode => _driver.hashCode;
+
+  @override
+  bool operator ==(other) => other is W3cKeyboard && other._driver == _driver;
+}
diff --git a/lib/src/sync/w3c_spec/web_driver.dart b/lib/src/sync/w3c_spec/web_driver.dart
index acbd3c1..15f08ad 100644
--- a/lib/src/sync/w3c_spec/web_driver.dart
+++ b/lib/src/sync/w3c_spec/web_driver.dart
@@ -16,6 +16,7 @@
 import 'package:stack_trace/stack_trace.dart' show Chain;
 
 import 'element_finder.dart';
+import 'keyboard.dart';
 import 'navigation.dart';
 import 'target_locator.dart';
 import 'timeouts.dart';
@@ -25,8 +26,8 @@
 import '../../../async_core.dart' as async_core;
 
 import '../common_spec/cookies.dart';
+import '../keyboard.dart';
 // We don't implement this, but we need the types to define the API.
-import '../json_wire_spec/keyboard.dart';
 import '../json_wire_spec/logs.dart';
 import '../json_wire_spec/mouse.dart';
 
@@ -140,8 +141,7 @@
   Logs get logs => throw 'Unsupported in W3C spec.';
 
   @override
-  Keyboard get keyboard =>
-      throw 'Unsupported in W3C spec, use Actions instead.';
+  Keyboard get keyboard => new W3cKeyboard(this);
 
   @override
   Mouse get mouse => throw 'Unsupported in W3C spec, use Actions instead.';
diff --git a/lib/src/sync/web_driver.dart b/lib/src/sync/web_driver.dart
index a77fa76..d2924e8 100644
--- a/lib/src/sync/web_driver.dart
+++ b/lib/src/sync/web_driver.dart
@@ -14,6 +14,7 @@
 
 import 'command_event.dart';
 import 'common.dart';
+import 'keyboard.dart';
 import 'navigation.dart';
 import 'target_locator.dart';
 import 'timeouts.dart';
@@ -26,7 +27,6 @@
 
 import 'json_wire_spec/mouse.dart';
 import 'json_wire_spec/logs.dart';
-import 'json_wire_spec/keyboard.dart';
 
 typedef void WebDriverListener(WebDriverCommandEvent event);
 
@@ -98,9 +98,6 @@
 
   Timeouts get timeouts;
 
-  // TODO(staats): add actions support.
-
-  @Deprecated('This not supported in the W3C spec. Use actions instead.')
   Keyboard get keyboard;
 
   @Deprecated('This not supported in the W3C spec. Use actions instead.')
diff --git a/lib/sync_core.dart b/lib/sync_core.dart
index 1ed14ba..03a396d 100644
--- a/lib/sync_core.dart
+++ b/lib/sync_core.dart
@@ -32,10 +32,10 @@
 export 'package:webdriver/src/sync/command_processor.dart';
 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/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/keyboard.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';
@@ -53,7 +53,7 @@
 WebDriver createDriver(
     {Uri uri,
     Map<String, dynamic> desired,
-    WebDriverSpec spec = WebDriverSpec.JsonWire}) {
+    WebDriverSpec spec = WebDriverSpec.Auto}) {
   uri ??= defaultUri;
 
   desired ??= Capabilities.empty;
@@ -71,14 +71,22 @@
       final processor =
           new SyncHttpCommandProcessor(processor: processW3cResponse);
       final response = processor.post(
-          uri.resolve('session'), {'desiredCapabilities': desired},
+          uri.resolve('session'),
+          {
+            'capabilities': {'desiredCapabilities': desired}
+          },
           value: true) as Map<String, dynamic>;
       return new w3c.W3cWebDriver(processor, uri, response['sessionId'],
           new UnmodifiableMapView(response['value'] as Map<String, dynamic>));
     case WebDriverSpec.Auto:
       final response =
           new SyncHttpCommandProcessor(processor: inferSessionResponseSpec)
-              .post(uri.resolve('session'), {'desiredCapabilities': desired},
+              .post(
+                  uri.resolve('session'),
+                  {
+                    'desiredCapabilities': desired,
+                    'capabilities': {'desiredCapabilities': desired}
+                  },
                   value: true) as InferredResponse;
       return fromExistingSession(response.sessionId,
           uri: uri, spec: response.spec);
diff --git a/lib/sync_io.dart b/lib/sync_io.dart
index 2a35e32..3c91056 100644
--- a/lib/sync_io.dart
+++ b/lib/sync_io.dart
@@ -28,7 +28,7 @@
 core.WebDriver createDriver(
         {Uri uri,
         Map<String, dynamic> desired,
-        core.WebDriverSpec spec = core.WebDriverSpec.JsonWire}) =>
+        core.WebDriverSpec spec = core.WebDriverSpec.Auto}) =>
     core.createDriver(uri: uri, desired: desired, spec: spec);
 
 /// Creates a WebDriver instance connected to an existing session.
diff --git a/test/firefox_w3c_keyboard_test.dart b/test/firefox_w3c_keyboard_test.dart
new file mode 100644
index 0000000..1c9f357
--- /dev/null
+++ b/test/firefox_w3c_keyboard_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/keyboard.dart';
+import 'sync/sync_io_config.dart' as config;
+
+void main() {
+  runTests(config.createFirefoxTestDriver);
+}
diff --git a/test/sync/keyboard.dart b/test/sync/keyboard.dart
index 405a7e8..2b8fe22 100644
--- a/test/sync/keyboard.dart
+++ b/test/sync/keyboard.dart
@@ -50,28 +50,37 @@
 
     test('sendKeys -- once', () {
       driver.keyboard.sendKeys('abcdef');
-      expect(textInput.attributes['value'], 'abcdef');
+      expect(valueOf(textInput), 'abcdef');
     });
 
     test('sendKeys -- twice', () {
       driver.keyboard.sendKeys('abc');
       driver.keyboard.sendKeys('def');
-      expect(textInput.attributes['value'], 'abcdef');
+      expect(valueOf(textInput), 'abcdef');
     });
 
     test('sendKeys -- with tab', () {
       driver.keyboard.sendKeys('abc${Keyboard.tab}def');
-      expect(textInput.attributes['value'], 'abc');
+      expect(valueOf(textInput), 'abc');
     });
 
     // NOTE: does not work on Mac.
     test('sendChord -- CTRL+X', () {
       driver.keyboard.sendKeys('abcdef');
-      expect(textInput.attributes['value'], 'abcdef');
+      expect(valueOf(textInput), 'abcdef');
       driver.keyboard.sendChord([ctrlCmdKey, 'a']);
       driver.keyboard.sendChord([ctrlCmdKey, 'x']);
       driver.keyboard.sendKeys('xxx');
-      expect(textInput.attributes['value'], 'xxx');
+      expect(valueOf(textInput), 'xxx');
     });
   }, timeout: const Timeout(const Duration(minutes: 2)));
 }
+
+/// Gets the "value" property of a [WebElement].
+///
+/// The behavior for the "value" property of a text input is different for
+/// different specs (json wire updates it through attribute and W3C updates it
+/// through property).
+String valueOf(WebElement textInput) {
+  return textInput.attributes['value'] ?? textInput.properties['value'];
+}
\ No newline at end of file