diff --git a/examples/stocks/lib/stock_list.dart b/examples/stocks/lib/stock_list.dart index 153db6926a..9634180c58 100644 --- a/examples/stocks/lib/stock_list.dart +++ b/examples/stocks/lib/stock_list.dart @@ -18,6 +18,7 @@ class StockList extends StatelessComponent { 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 5c4516c3fd..abaed7e212 100644 --- a/examples/stocks/pubspec.yaml +++ b/examples/stocks/pubspec.yaml @@ -7,3 +7,5 @@ dependencies: 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 0000000000..623f7b7505 --- /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 0000000000..62cb9e6dfd --- /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 05c1da07c8..74a2386f33 100644 --- a/packages/flutter_driver/lib/src/driver.dart +++ b/packages/flutter_driver/lib/src/driver.dart @@ -148,7 +148,7 @@ class FlutterDriver { final VMIsolateRef _appIsolate; Future> _sendCommand(Command command) async { - Map json = {'kind': command.kind} + Map json = {'command': command.kind} ..addAll(command.toJson()); return _appIsolate.invokeExtension(_kFlutterExtensionMethod, json) .then((Map result) => result, onError: (error, stackTrace) { @@ -184,6 +184,23 @@ class FlutterDriver { 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 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 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 cc5ff6ddf4..9b57ab2358 100644 --- a/packages/flutter_driver/lib/src/extension.dart +++ b/packages/flutter_driver/lib/src/extension.dart @@ -7,7 +7,9 @@ import 'dart:convert'; 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 @@ class FlutterDriverExtension { 'find': find, 'tap': tap, 'get_text': getText, + 'scroll': scroll, }; _commandDeserializers = { @@ -61,6 +64,7 @@ class FlutterDriverExtension { 'find': Find.fromJson, 'tap': Tap.fromJson, 'get_text': GetText.fromJson, + 'scroll': Scroll.fromJson, }; } @@ -74,7 +78,7 @@ class FlutterDriverExtension { Future call(Map params) async { try { - String commandKind = params['kind']; + String commandKind = params['command']; CommandHandlerCallback commandHandler = _commandHandlers[commandKind]; CommandDeserializerCallback commandDeserializer = _commandDeserializers[commandKind]; @@ -91,11 +95,13 @@ class FlutterDriverExtension { 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 @@ class FlutterDriverExtension { return new TapResult(); } + Future 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.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.delayed(pause); + } + prober.dispatchEvent(pointer.up(), hitTest); + + return new ScrollResult(); + } + Future 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 2e44a3bd63..b4927ec6d1 100644 --- a/packages/flutter_driver/lib/src/gesture.dart +++ b/packages/flutter_driver/lib/src/gesture.dart @@ -23,3 +23,54 @@ class TapResult extends Result { Map 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 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 toJson() => super.toJson()..addAll({ + 'dx': dx, + 'dy': dy, + 'duration': duration.inMicroseconds, + 'frequency': frequency, + }); +} + +class ScrollResult extends Result { + static ScrollResult fromJson(Map json) { + return new ScrollResult(); + } + + Map toJson() => {}; +} diff --git a/packages/flutter_driver/test/flutter_driver_test.dart b/packages/flutter_driver/test/flutter_driver_test.dart index b1bf2e5fd0..4ed75da944 100644 --- a/packages/flutter_driver/test/flutter_driver_test.dart +++ b/packages/flutter_driver/test/flutter_driver_test.dart @@ -122,7 +122,7 @@ main() { 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 @@ main() { 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 @@ main() { 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({