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({