driver.scroll action; scroll perf test for Stocks
diff --git a/examples/stocks/lib/stock_list.dart b/examples/stocks/lib/stock_list.dart
index 153db69..9634180 100644
--- a/examples/stocks/lib/stock_list.dart
+++ b/examples/stocks/lib/stock_list.dart
@@ -18,6 +18,7 @@
 
   Widget build(BuildContext context) {
     return new ScrollableList(
+      key: const ValueKey('stock-list'),
       itemExtent: StockRow.kHeight,
       children: stocks.map((Stock stock) {
         return new StockRow(
diff --git a/examples/stocks/pubspec.yaml b/examples/stocks/pubspec.yaml
index 5c4516c..abaed7e 100644
--- a/examples/stocks/pubspec.yaml
+++ b/examples/stocks/pubspec.yaml
@@ -7,3 +7,5 @@
 dev_dependencies:
   flutter_test:
     path: ../../packages/flutter_test
+  flutter_driver:
+    path: ../../packages/flutter_driver
diff --git a/examples/stocks/test_driver/scroll_perf.dart b/examples/stocks/test_driver/scroll_perf.dart
new file mode 100644
index 0000000..623f7b7
--- /dev/null
+++ b/examples/stocks/test_driver/scroll_perf.dart
@@ -0,0 +1,13 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_driver/driver_extension.dart';
+import 'package:flutter_driver/src/error.dart';
+import 'package:stocks/main.dart' as app;
+
+void main() {
+  flutterDriverLog.listen(print);
+  enableFlutterDriverExtension();
+  app.main();
+}
diff --git a/examples/stocks/test_driver/scroll_perf_test.dart b/examples/stocks/test_driver/scroll_perf_test.dart
new file mode 100644
index 0000000..62cb9e6
--- /dev/null
+++ b/examples/stocks/test_driver/scroll_perf_test.dart
@@ -0,0 +1,40 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'package:flutter_driver/flutter_driver.dart';
+import 'package:test/test.dart';
+
+main() {
+  group('scrolling performance test', () {
+    FlutterDriver driver;
+
+    setUpAll(() async {
+      driver = await FlutterDriver.connect();
+    });
+
+    tearDownAll(() async {
+      if (driver != null)
+        driver.close();
+    });
+
+    test('tap on the floating action button; verify counter', () async {
+      // Find the scrollable stock list
+      ObjectRef stockList = await driver.findByValueKey('stock-list');
+      expect(stockList, isNotNull);
+
+      // Scroll down 5 times
+      for (int i = 0; i < 5; i++) {
+        await driver.scroll(stockList, 0.0, -300.0, new Duration(milliseconds: 300));
+        await new Future.delayed(new Duration(milliseconds: 500));
+      }
+
+      // Scroll up 5 times
+      for (int i = 0; i < 5; i++) {
+        await driver.scroll(stockList, 0.0, 300.0, new Duration(milliseconds: 300));
+        await new Future.delayed(new Duration(milliseconds: 500));
+      }
+    });
+  });
+}
diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart
index 05c1da0..74a2386 100644
--- a/packages/flutter_driver/lib/src/driver.dart
+++ b/packages/flutter_driver/lib/src/driver.dart
@@ -148,7 +148,7 @@
   final VMIsolateRef _appIsolate;
 
   Future<Map<String, dynamic>> _sendCommand(Command command) async {
-    Map<String, dynamic> json = <String, dynamic>{'kind': command.kind}
+    Map<String, dynamic> json = <String, dynamic>{'command': command.kind}
       ..addAll(command.toJson());
     return _appIsolate.invokeExtension(_kFlutterExtensionMethod, json)
       .then((Map<String, dynamic> result) => result, onError: (error, stackTrace) {
@@ -184,6 +184,23 @@
     return await _sendCommand(new Tap(ref)).then((_) => null);
   }
 
+  /// Tell the driver to perform a scrolling action.
+  ///
+  /// A scrolling action begins with a "pointer down" event, which commonly maps
+  /// to finger press on the touch screen or mouse button press. A series of
+  /// "pointer move" events follow. The action is completed by a "pointer up"
+  /// event.
+  ///
+  /// [dx] and [dy] specify the total offset for the entire scrolling action.
+  ///
+  /// [duration] specifies the lenght of the action.
+  ///
+  /// The move events are generated at a given [frequency] in Hz (or events per
+  /// second). It defaults to 60Hz.
+  Future<Null> scroll(ObjectRef ref, double dx, double dy, Duration duration, {int frequency: 60}) async {
+    return await _sendCommand(new Scroll(ref, dx, dy, duration, frequency)).then((_) => null);
+  }
+
   Future<String> getText(ObjectRef ref) async {
     GetTextResult result = GetTextResult.fromJson(await _sendCommand(new GetText(ref)));
     return result.text;
diff --git a/packages/flutter_driver/lib/src/extension.dart b/packages/flutter_driver/lib/src/extension.dart
index cc5ff6d..9b57ab2 100644
--- a/packages/flutter_driver/lib/src/extension.dart
+++ b/packages/flutter_driver/lib/src/extension.dart
@@ -7,7 +7,9 @@
 import 'dart:developer';
 import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
+import 'package:flutter/gestures.dart';
 import 'package:flutter_test/src/instrumentation.dart';
+import 'package:flutter_test/src/test_pointer.dart';
 
 import 'error.dart';
 import 'find.dart';
@@ -54,6 +56,7 @@
       'find': find,
       'tap': tap,
       'get_text': getText,
+      'scroll': scroll,
     };
 
     _commandDeserializers = {
@@ -61,6 +64,7 @@
       'find': Find.fromJson,
       'tap': Tap.fromJson,
       'get_text': GetText.fromJson,
+      'scroll': Scroll.fromJson,
     };
   }
 
@@ -74,7 +78,7 @@
 
   Future<ServiceExtensionResponse> call(Map<String, String> params) async {
     try {
-      String commandKind = params['kind'];
+      String commandKind = params['command'];
       CommandHandlerCallback commandHandler = _commandHandlers[commandKind];
       CommandDeserializerCallback commandDeserializer =
           _commandDeserializers[commandKind];
@@ -91,11 +95,13 @@
         return new ServiceExtensionResponse.result(JSON.encode(result.toJson()));
       }, onError: (e, s) {
         _log.warning('$e:\n$s');
-        return new ServiceExtensionResponse.error(
-          ServiceExtensionResponse.kExtensionError, '$e');
+        return new ServiceExtensionResponse.error(ServiceExtensionResponse.kExtensionError, '$e');
       });
     } catch(error, stackTrace) {
-      _log.warning('Uncaught extension error: $error\n$stackTrace');
+      String message = 'Uncaught extension error: $error\n$stackTrace';
+      _log.error(message);
+      return new ServiceExtensionResponse.error(
+        ServiceExtensionResponse.kExtensionError, message);
     }
   }
 
@@ -168,6 +174,29 @@
     return new TapResult();
   }
 
+  Future<ScrollResult> scroll(Scroll command) async {
+    Element target = await _dereferenceOrDie(command.targetRef);
+    final int totalMoves = command.duration.inMicroseconds * command.frequency ~/ Duration.MICROSECONDS_PER_SECOND;
+    Offset delta = new Offset(command.dx, command.dy) / totalMoves.toDouble();
+    Duration pause = command.duration ~/ totalMoves;
+    Point startLocation = prober.getCenter(target);
+    Point currentLocation = startLocation;
+    TestPointer pointer = new TestPointer(1);
+    HitTestResult hitTest = new HitTestResult();
+
+    prober.binding.hitTest(hitTest, startLocation);
+    prober.dispatchEvent(pointer.down(startLocation), hitTest);
+    await new Future<Null>.value();  // so that down and move don't happen in the same microtask
+    for (int moves = 0; moves < totalMoves; moves++) {
+      currentLocation = currentLocation + delta;
+      prober.dispatchEvent(pointer.move(currentLocation), hitTest);
+      await new Future<Null>.delayed(pause);
+    }
+    prober.dispatchEvent(pointer.up(), hitTest);
+
+    return new ScrollResult();
+  }
+
   Future<GetTextResult> getText(GetText command) async {
     Element target = await _dereferenceOrDie(command.targetRef);
     // TODO(yjbanov): support more ways to read text
diff --git a/packages/flutter_driver/lib/src/gesture.dart b/packages/flutter_driver/lib/src/gesture.dart
index 2e44a3b..b4927ec 100644
--- a/packages/flutter_driver/lib/src/gesture.dart
+++ b/packages/flutter_driver/lib/src/gesture.dart
@@ -23,3 +23,54 @@
 
   Map<String, dynamic> toJson() => {};
 }
+
+
+/// Command the driver to perform a scrolling action.
+class Scroll extends CommandWithTarget {
+  final String kind = 'scroll';
+
+  Scroll(
+    ObjectRef targetRef,
+    this.dx,
+    this.dy,
+    this.duration,
+    this.frequency
+  ) : super(targetRef);
+
+  static Scroll fromJson(Map<String, dynamic> json) {
+    return new Scroll(
+      new ObjectRef(json['targetRef']),
+      double.parse(json['dx']),
+      double.parse(json['dy']),
+      new Duration(microseconds: int.parse(json['duration'])),
+      int.parse(json['frequency'])
+    );
+  }
+
+  /// Delta X offset per move event.
+  final double dx;
+
+  /// Delta Y offset per move event.
+  final double dy;
+
+  /// The duration of the scrolling action
+  final Duration duration;
+
+  /// The frequency in Hz of the generated move events.
+  final int frequency;
+
+  Map<String, dynamic> toJson() => super.toJson()..addAll({
+    'dx': dx,
+    'dy': dy,
+    'duration': duration.inMicroseconds,
+    'frequency': frequency,
+  });
+}
+
+class ScrollResult extends Result {
+  static ScrollResult fromJson(Map<String, dynamic> json) {
+    return new ScrollResult();
+  }
+
+  Map<String, dynamic> toJson() => {};
+}
diff --git a/packages/flutter_driver/test/flutter_driver_test.dart b/packages/flutter_driver/test/flutter_driver_test.dart
index b1bf2e5..4ed75da 100644
--- a/packages/flutter_driver/test/flutter_driver_test.dart
+++ b/packages/flutter_driver/test/flutter_driver_test.dart
@@ -122,7 +122,7 @@
       test('finds by ValueKey', () async {
         when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
           expect(i.positionalArguments[1], {
-            'kind': 'find',
+            'command': 'find',
             'searchSpecType': 'ByValueKey',
             'keyValueString': 'foo',
             'keyValueType': 'String'
@@ -150,7 +150,7 @@
       test('sends the tap command', () async {
         when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
           expect(i.positionalArguments[1], {
-            'kind': 'tap',
+            'command': 'tap',
             'targetRef': '123'
           });
           return new Future.value();
@@ -172,7 +172,7 @@
       test('sends the getText command', () async {
         when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
           expect(i.positionalArguments[1], {
-            'kind': 'get_text',
+            'command': 'get_text',
             'targetRef': '123'
           });
           return new Future.value({