diff --git a/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart b/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart index 749c5e1829a..66c577c1c55 100644 --- a/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart +++ b/dev/benchmarks/complex_layout/test_driver/scroll_perf_test.dart @@ -22,7 +22,7 @@ void main() { test('measure', () async { Timeline timeline = await driver.traceAction(() async { // Find the scrollable stock list - ObjectRef stockList = await driver.findByValueKey('main-scroll'); + SerializableFinder stockList = find.byValueKey('main-scroll'); expect(stockList, isNotNull); // Scroll down diff --git a/examples/material_gallery/test_driver/scroll_perf_test.dart b/examples/material_gallery/test_driver/scroll_perf_test.dart index f83a774db48..c4795b5abc9 100644 --- a/examples/material_gallery/test_driver/scroll_perf_test.dart +++ b/examples/material_gallery/test_driver/scroll_perf_test.dart @@ -22,12 +22,12 @@ void main() { test('measure', () async { Timeline timeline = await driver.traceAction(() async { // Find the scrollable stock list - ObjectRef stockList = await driver.findByValueKey('Gallery List'); + SerializableFinder stockList = find.byValueKey('Gallery List'); expect(stockList, isNotNull); - await driver.tap(await driver.findByText('Demos')); - await driver.tap(await driver.findByText('Components')); - await driver.tap(await driver.findByText('Style')); + await driver.tap(find.text('Demos')); + await driver.tap(find.text('Components')); + await driver.tap(find.text('Style')); // TODO(eseidel): These are very artifical scrolls, we should use better // https://github.com/flutter/flutter/issues/3316 diff --git a/examples/stocks/test_driver/scroll_perf_test.dart b/examples/stocks/test_driver/scroll_perf_test.dart index 5a61bf59b32..bfeb4ec8dde 100644 --- a/examples/stocks/test_driver/scroll_perf_test.dart +++ b/examples/stocks/test_driver/scroll_perf_test.dart @@ -23,7 +23,7 @@ void main() { test('measure', () async { Timeline timeline = await driver.traceAction(() async { // Find the scrollable stock list - ObjectRef stockList = await driver.findByValueKey('stock-list'); + SerializableFinder stockList = find.byValueKey('stock-list'); expect(stockList, isNotNull); // Scroll down diff --git a/packages/flutter_driver/lib/flutter_driver.dart b/packages/flutter_driver/lib/flutter_driver.dart index 8d2255d56b4..954dcd6981c 100644 --- a/packages/flutter_driver/lib/flutter_driver.dart +++ b/packages/flutter_driver/lib/flutter_driver.dart @@ -12,6 +12,9 @@ library flutter_driver; export 'src/driver.dart' show + find, + CommonFinders, + EvaluatorFunction, FlutterDriver; export 'src/error.dart' show @@ -21,7 +24,7 @@ export 'src/error.dart' show flutterDriverLog; export 'src/find.dart' show - ObjectRef, + SerializableFinder, GetTextResult; export 'src/health.dart' show @@ -31,7 +34,6 @@ export 'src/health.dart' show export 'src/message.dart' show Message, Command, - ObjectRef, CommandWithTarget, Result; diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart index 8e38209421d..f4eec6906af 100644 --- a/packages/flutter_driver/lib/src/driver.dart +++ b/packages/flutter_driver/lib/src/driver.dart @@ -20,6 +20,14 @@ import 'timeline.dart'; final Logger _log = new Logger('FlutterDriver'); +/// A convenient accessor to frequently used finders. +/// +/// Examples: +/// +/// driver.tap(find.byText('Save')); +/// driver.scroll(find.byValueKey(42)); +const CommonFinders find = const CommonFinders._(); + /// Computes a value. /// /// If computation is asynchronous, the function may return a [Future]. @@ -162,14 +170,15 @@ class FlutterDriver { Future> _sendCommand(Command command) async { Map parameters = {'command': command.kind} ..addAll(command.serialize()); - return _appIsolate.invokeExtension(_kFlutterExtensionMethod, parameters) - .then((Map result) => result, onError: (dynamic error, dynamic stackTrace) { - throw new DriverError( - 'Failed to fulfill ${command.runtimeType} due to remote error', - error, - stackTrace - ); - }); + try { + return await _appIsolate.invokeExtension(_kFlutterExtensionMethod, parameters); + } catch (error, stackTrace) { + throw new DriverError( + 'Failed to fulfill ${command.runtimeType} due to remote error', + error, + stackTrace + ); + } } /// Checks the status of the Flutter Driver extension. @@ -177,23 +186,14 @@ class FlutterDriver { return Health.fromJson(await _sendCommand(new GetHealth())); } - /// Finds the UI element with the given [key]. - Future findByValueKey(dynamic key) async { - return ObjectRef.fromJson(await _sendCommand(new Find(new ByValueKey(key)))); + /// Taps at the center of the widget located by [finder]. + Future tap(SerializableFinder finder) async { + return await _sendCommand(new Tap(finder)).then((Map _) => null); } - /// Finds the UI element for the tooltip with the given [message]. - Future findByTooltipMessage(String message) async { - return ObjectRef.fromJson(await _sendCommand(new Find(new ByTooltipMessage(message)))); - } - - /// Finds the text element with the given [text]. - Future findByText(String text) async { - return ObjectRef.fromJson(await _sendCommand(new Find(new ByText(text)))); - } - - Future tap(ObjectRef ref) async { - return await _sendCommand(new Tap(ref)).then((Map _) => null); + /// Whether at least one widget identified by [finder] exists on the UI. + Future exists(SerializableFinder finder) async { + return await _sendCommand(new Exists(finder)).then((Map _) => null); } /// Tell the driver to perform a scrolling action. @@ -209,13 +209,13 @@ class FlutterDriver { /// /// 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((Map _) => null); + Future scroll(SerializableFinder finder, double dx, double dy, Duration duration, {int frequency: 60}) async { + return await _sendCommand(new Scroll(finder, dx, dy, duration, frequency)).then((Map _) => null); } - Future getText(ObjectRef ref) async { - GetTextResult result = GetTextResult.fromJson(await _sendCommand(new GetText(ref))); - return result.text; + /// Returns the text in the `Text` widget located by [finder]. + Future getText(SerializableFinder finder) async { + return GetTextResult.fromJson(await _sendCommand(new GetText(finder))).text; } /// Starts recording performance traces. @@ -358,3 +358,17 @@ Future _waitAndConnect(String url) async { return attemptConnection(); } + +/// Provides convenient accessors to frequently used finders. +class CommonFinders { + const CommonFinders._(); + + /// Finds [Text] widgets containing string equal to [text]. + SerializableFinder text(String text) => new ByText(text); + + /// Finds widgets by [key]. + SerializableFinder byValueKey(dynamic key) => new ByValueKey(key); + + /// Finds widgets with a tooltip with the given [message]. + SerializableFinder byTooltip(String message) => new ByTooltipMessage(message); +} diff --git a/packages/flutter_driver/lib/src/extension.dart b/packages/flutter_driver/lib/src/extension.dart index 644557101a0..529b89a33e2 100644 --- a/packages/flutter_driver/lib/src/extension.dart +++ b/packages/flutter_driver/lib/src/extension.dart @@ -47,34 +47,39 @@ typedef Future CommandHandlerCallback(Command c); /// Deserializes JSON map to a command object. typedef Command CommandDeserializerCallback(Map params); +/// Runs the finder and returns the [Element] found, or `null`. +typedef Future FinderCallback(SerializableFinder finder); + class FlutterDriverExtension { static final Logger _log = new Logger('FlutterDriverExtension'); FlutterDriverExtension() { - _commandHandlers = { + _commandHandlers = { 'get_health': getHealth, - 'find': find, 'tap': tap, 'get_text': getText, 'scroll': scroll, }; - _commandDeserializers = { + _commandDeserializers = { 'get_health': GetHealth.deserialize, - 'find': Find.deserialize, 'tap': Tap.deserialize, 'get_text': GetText.deserialize, 'scroll': Scroll.deserialize, }; + + _finders = { + 'ByValueKey': _findByValueKey, + 'ByTooltipMessage': _findByTooltipMessage, + 'ByText': _findByText, + }; } final Instrumentation prober = new Instrumentation(); - Map _commandHandlers = - {}; - - Map _commandDeserializers = - {}; + Map _commandHandlers; + Map _commandDeserializers; + Map _finders; Future call(Map params) async { try { @@ -107,36 +112,18 @@ class FlutterDriverExtension { Future getHealth(GetHealth command) async => new Health(HealthStatus.ok); - Future find(Find command) async { - SearchSpecification searchSpec = command.searchSpec; - switch(searchSpec.runtimeType) { - case ByValueKey: return findByValueKey(searchSpec); - case ByTooltipMessage: return findByTooltipMessage(searchSpec); - case ByText: return findByText(searchSpec); - } - throw new DriverError('Unsupported search specification type ${searchSpec.runtimeType}'); - } - - /// Runs object [locator] repeatedly until it returns a non-`null` value. - /// - /// [descriptionGetter] describes the object to be waited for. It is used in - /// the warning printed should timeout happen. - Future _waitForObject(String descriptionGetter(), Object locator()) async { - Object object = await retry(locator, _kDefaultTimeout, _kDefaultPauseBetweenRetries, predicate: (Object object) { + /// Runs object [finder] repeatedly until it finds an [Element]. + Future _waitForElement(String descriptionGetter(), Element locator()) { + return retry(locator, _kDefaultTimeout, _kDefaultPauseBetweenRetries, predicate: (dynamic object) { return object != null; }).catchError((Object error, Object stackTrace) { _log.warning('Timed out waiting for ${descriptionGetter()}'); return null; }); - - ObjectRef elemRef = object != null - ? new ObjectRef(_registerObject(object)) - : new ObjectRef.notFound(); - return new Future.value(elemRef); } - Future findByValueKey(ByValueKey byKey) async { - return _waitForObject( + Future _findByValueKey(ByValueKey byKey) async { + return _waitForElement( () => 'element with key "${byKey.keyValue}" of type ${byKey.keyValueType}', () { return prober.findElementByKey(new ValueKey(byKey.keyValue)); @@ -144,8 +131,8 @@ class FlutterDriverExtension { ); } - Future findByTooltipMessage(ByTooltipMessage byTooltipMessage) async { - return _waitForObject( + Future _findByTooltipMessage(ByTooltipMessage byTooltipMessage) async { + return _waitForElement( () => 'tooltip with message "${byTooltipMessage.text}" on it', () { return prober.findElement((Element element) { @@ -160,22 +147,31 @@ class FlutterDriverExtension { ); } - Future findByText(ByText byText) async { - return await _waitForObject( + Future _findByText(ByText byText) async { + return await _waitForElement( () => 'text "${byText.text}"', () { return prober.findText(byText.text); }); } + Future _runFinder(SerializableFinder finder) { + FinderCallback cb = _finders[finder.finderType]; + + if (cb == null) + throw 'Unsupported finder type: ${finder.finderType}'; + + return cb(finder); + } + Future tap(Tap command) async { - Element target = await _dereferenceOrDie(command.targetRef); + Element target = await _runFinder(command.finder); prober.tap(target); return new TapResult(); } Future scroll(Scroll command) async { - Element target = await _dereferenceOrDie(command.targetRef); + Element target = await _runFinder(command.finder); 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; @@ -198,30 +194,9 @@ class FlutterDriverExtension { } Future getText(GetText command) async { - Element target = await _dereferenceOrDie(command.targetRef); + Element target = await _runFinder(command.finder); // TODO(yjbanov): support more ways to read text Text text = target.widget; return new GetTextResult(text.data); } - - int _refCounter = 1; - final Map _objectRefs = {}; - String _registerObject(Object obj) { - if (obj == null) - throw new ArgumentError('Cannot register null object'); - String refKey = '${_refCounter++}'; - _objectRefs[refKey] = obj; - return refKey; - } - - dynamic _dereference(String reference) => _objectRefs[reference]; - - Future _dereferenceOrDie(String reference) { - Element object = _dereference(reference); - - if (object == null) - return new Future.error('Object reference not found ($reference).'); - - return new Future.value(object); - } } diff --git a/packages/flutter_driver/lib/src/find.dart b/packages/flutter_driver/lib/src/find.dart index 2b63a9c3988..589f66a4b95 100644 --- a/packages/flutter_driver/lib/src/find.dart +++ b/packages/flutter_driver/lib/src/find.dart @@ -11,46 +11,86 @@ DriverError _createInvalidKeyValueTypeError(String invalidType) { return new DriverError('Unsupported key value type $invalidType. Flutter Driver only supports ${_supportedKeyValueTypes.join(", ")}'); } -/// Command to find an element. -class Find extends Command { - @override - final String kind = 'find'; - - Find(this.searchSpec); - - final SearchSpecification searchSpec; - - @override - Map serialize() => searchSpec.serialize(); - - static Find deserialize(Map json) { - return new Find(SearchSpecification.deserialize(json)); +/// A command aimed at an object to be located by [finder]. +/// +/// Implementations must provide a concrete [kind]. If additional data is +/// required beyond the [finder] the implementation may override [serialize] +/// and add more keys to the returned map. +abstract class CommandWithTarget extends Command { + CommandWithTarget(this.finder) { + if (finder == null) + throw new DriverError('${this.runtimeType} target cannot be null'); } + + /// Locates the object or objects targeted by this command. + final SerializableFinder finder; + + /// This method is meant to be overridden if data in addition to [finder] + /// is serialized to JSON. + /// + /// Example: + /// + /// Map toJson() => super.toJson()..addAll({ + /// 'foo': this.foo, + /// }); + @override + Map serialize() => finder.serialize(); +} + +/// Checks if the widget identified by the given finder exists. +class Exists extends CommandWithTarget { + @override + final String kind = 'exists'; + + Exists(SerializableFinder finder) : super(finder); + + static Exists deserialize(Map json) { + return new Exists(SerializableFinder.deserialize(json)); + } + + @override + Map serialize() => super.serialize(); +} + +class ExistsResult extends Result { + ExistsResult(this.exists); + + static ExistsResult fromJson(Map json) { + return new ExistsResult(json['exists']); + } + + /// Whether the widget was found on the UI or not. + final bool exists; + + @override + Map toJson() => { + 'exists': exists, + }; } /// Describes how to the driver should search for elements. -abstract class SearchSpecification { - String get searchSpecType; +abstract class SerializableFinder { + String get finderType; - static SearchSpecification deserialize(Map json) { - String searchSpecType = json['searchSpecType']; - switch(searchSpecType) { + static SerializableFinder deserialize(Map json) { + String finderType = json['finderType']; + switch(finderType) { case 'ByValueKey': return ByValueKey.deserialize(json); case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json); case 'ByText': return ByText.deserialize(json); } - throw new DriverError('Unsupported search specification type $searchSpecType'); + throw new DriverError('Unsupported search specification type $finderType'); } Map serialize() => { - 'searchSpecType': searchSpecType, + 'finderType': finderType, }; } -/// Tells [Find] to search by tooltip text. -class ByTooltipMessage extends SearchSpecification { +/// Finds widgets by tooltip text. +class ByTooltipMessage extends SerializableFinder { @override - final String searchSpecType = 'ByTooltipMessage'; + final String finderType = 'ByTooltipMessage'; ByTooltipMessage(this.text); @@ -67,10 +107,10 @@ class ByTooltipMessage extends SearchSpecification { } } -/// Tells [Find] to search for `Text` widget by text. -class ByText extends SearchSpecification { +/// Finds widgets by [text] inside a `Text` widget. +class ByText extends SerializableFinder { @override - final String searchSpecType = 'ByText'; + final String finderType = 'ByText'; ByText(this.text); @@ -86,10 +126,10 @@ class ByText extends SearchSpecification { } } -/// Tells [Find] to search by `ValueKey`. -class ByValueKey extends SearchSpecification { +/// Finds widgets by `ValueKey`. +class ByValueKey extends SerializableFinder { @override - final String searchSpecType = 'ByValueKey'; + final String finderType = 'ByValueKey'; ByValueKey(dynamic keyValue) : this.keyValue = keyValue, @@ -132,14 +172,14 @@ class ByValueKey extends SearchSpecification { /// Command to read the text from a given element. class GetText extends CommandWithTarget { - /// [targetRef] identifies an element that contains a piece of text. - GetText(ObjectRef targetRef) : super(targetRef); + /// [finder] looks for an element that contains a piece of text. + GetText(SerializableFinder finder) : super(finder); @override final String kind = 'get_text'; static GetText deserialize(Map json) { - return new GetText(new ObjectRef(json['targetRef'])); + return new GetText(SerializableFinder.deserialize(json)); } @override diff --git a/packages/flutter_driver/lib/src/gesture.dart b/packages/flutter_driver/lib/src/gesture.dart index f040d64288e..26dc6a1fe40 100644 --- a/packages/flutter_driver/lib/src/gesture.dart +++ b/packages/flutter_driver/lib/src/gesture.dart @@ -3,15 +3,16 @@ // found in the LICENSE file. import 'message.dart'; +import 'find.dart'; class Tap extends CommandWithTarget { @override final String kind = 'tap'; - Tap(ObjectRef targetRef) : super(targetRef); + Tap(SerializableFinder finder) : super(finder); static Tap deserialize(Map json) { - return new Tap(new ObjectRef(json['targetRef'])); + return new Tap(SerializableFinder.deserialize(json)); } @override @@ -34,16 +35,16 @@ class Scroll extends CommandWithTarget { final String kind = 'scroll'; Scroll( - ObjectRef targetRef, + SerializableFinder finder, this.dx, this.dy, this.duration, this.frequency - ) : super(targetRef); + ) : super(finder); static Scroll deserialize(Map json) { return new Scroll( - new ObjectRef(json['targetRef']), + SerializableFinder.deserialize(json), double.parse(json['dx']), double.parse(json['dy']), new Duration(microseconds: int.parse(json['duration'])), diff --git a/packages/flutter_driver/lib/src/message.dart b/packages/flutter_driver/lib/src/message.dart index 374d98583a8..b4a4d500826 100644 --- a/packages/flutter_driver/lib/src/message.dart +++ b/packages/flutter_driver/lib/src/message.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'error.dart'; - /// An object sent from the Flutter Driver to a Flutter application to instruct /// the application to perform a task. abstract class Command { @@ -20,58 +18,3 @@ abstract class Result { // ignore: one_member_abstracts /// Serializes this message to a JSON map. Map toJson(); } - -/// A serializable reference to an object that lives in the application isolate. -class ObjectRef extends Result { - ObjectRef(this.objectReferenceKey); - - ObjectRef.notFound() : this(null); - - static ObjectRef fromJson(Map json) { - return json['objectReferenceKey'] != null - ? new ObjectRef(json['objectReferenceKey']) - : null; - } - - /// Identifier used to dereference an object. - /// - /// This value is generated by the application-side isolate. Flutter driver - /// tests should not generate these keys. - final String objectReferenceKey; - - @override - Map toJson() => { - 'objectReferenceKey': objectReferenceKey, - }; -} - -/// A command aimed at an object represented by [targetRef]. -/// -/// Implementations must provide a concrete [kind]. If additional data is -/// required beyond the [targetRef] the implementation may override [serialize] -/// and add more keys to the returned map. -abstract class CommandWithTarget extends Command { - CommandWithTarget(ObjectRef ref) : this.targetRef = ref?.objectReferenceKey { - if (ref == null) - throw new DriverError('${this.runtimeType} target cannot be null'); - - if (ref.objectReferenceKey == null) - throw new DriverError('${this.runtimeType} target reference cannot be null'); - } - - /// Refers to the object targeted by this command. - final String targetRef; - - /// This method is meant to be overridden if data in addition to [targetRef] - /// is serialized to JSON. - /// - /// Example: - /// - /// Map toJson() => super.toJson()..addAll({ - /// 'foo': this.foo, - /// }); - @override - Map serialize() => { - 'targetRef': targetRef, - }; -} diff --git a/packages/flutter_driver/test/flutter_driver_test.dart b/packages/flutter_driver/test/flutter_driver_test.dart index 602092734ce..1ff6cf04bb0 100644 --- a/packages/flutter_driver/test/flutter_driver_test.dart +++ b/packages/flutter_driver/test/flutter_driver_test.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'package:flutter_driver/src/driver.dart'; import 'package:flutter_driver/src/error.dart'; import 'package:flutter_driver/src/health.dart'; -import 'package:flutter_driver/src/message.dart'; import 'package:flutter_driver/src/timeline.dart'; import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:mockito/mockito.dart'; @@ -120,27 +119,23 @@ void main() { await driver.close(); }); - group('findByValueKey', () { + group('ByValueKey', () { test('restricts value types', () async { - expect(driver.findByValueKey(null), + expect(() => find.byValueKey(null), throwsA(new isInstanceOf())); }); test('finds by ValueKey', () async { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { expect(i.positionalArguments[1], { - 'command': 'find', - 'searchSpecType': 'ByValueKey', + 'command': 'tap', + 'finderType': 'ByValueKey', 'keyValueString': 'foo', 'keyValueType': 'String' }); - return new Future>.value({ - 'objectReferenceKey': '123', - }); + return new Future.value(); }); - ObjectRef result = await driver.findByValueKey('foo'); - expect(result, isNotNull); - expect(result.objectReferenceKey, '123'); + await driver.tap(find.byValueKey('foo')); }); }); @@ -149,20 +144,16 @@ void main() { expect(driver.tap(null), throwsA(new isInstanceOf())); }); - test('requires a valid target reference', () async { - expect(driver.tap(new ObjectRef.notFound()), - throwsA(new isInstanceOf())); - }); - test('sends the tap command', () async { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { expect(i.positionalArguments[1], { 'command': 'tap', - 'targetRef': '123' + 'finderType': 'ByText', + 'text': 'foo', }); return new Future>.value(); }); - await driver.tap(new ObjectRef('123')); + await driver.tap(find.text('foo')); }); }); @@ -171,22 +162,19 @@ void main() { expect(driver.getText(null), throwsA(new isInstanceOf())); }); - test('requires a valid target reference', () async { - expect(driver.getText(new ObjectRef.notFound()), - throwsA(new isInstanceOf())); - }); - test('sends the getText command', () async { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { expect(i.positionalArguments[1], { 'command': 'get_text', - 'targetRef': '123' + 'finderType': 'ByValueKey', + 'keyValueString': '123', + 'keyValueType': 'int' }); return new Future>.value({ 'text': 'hello' }); }); - String result = await driver.getText(new ObjectRef('123')); + String result = await driver.getText(find.byValueKey(123)); expect(result, 'hello'); }); }); diff --git a/packages/flutter_tools/templates/driver/main_test.dart.tmpl b/packages/flutter_tools/templates/driver/main_test.dart.tmpl index 3da4440dc32..3803f6296ae 100644 --- a/packages/flutter_tools/templates/driver/main_test.dart.tmpl +++ b/packages/flutter_tools/templates/driver/main_test.dart.tmpl @@ -23,14 +23,14 @@ void main() { test('tap on the floating action button; verify counter', () async { // Find floating action button (fab) to tap on - ObjectRef fab = await driver.findByTooltipMessage('Increment'); - expect(fab, isNotNull); + SerializableFinder fab = find.byTooltip('Increment'); + expect(await driver.exists(fab), isTrue); // Tap on the fab await driver.tap(fab); // Wait for text to change to the desired value - expect(await driver.findByText('Button tapped 1 time.'), isNotNull); + expect(await driver.exists(find.text('Button tapped 1 time.')), isTrue); }); }); }