Commit 9ce995f6 authored by Yegor's avatar Yegor

[driver] refactor API to finder objects (#3365)

parent a91bc0ba
...@@ -22,7 +22,7 @@ void main() { ...@@ -22,7 +22,7 @@ void main() {
test('measure', () async { test('measure', () async {
Timeline timeline = await driver.traceAction(() async { Timeline timeline = await driver.traceAction(() async {
// Find the scrollable stock list // Find the scrollable stock list
ObjectRef stockList = await driver.findByValueKey('main-scroll'); SerializableFinder stockList = find.byValueKey('main-scroll');
expect(stockList, isNotNull); expect(stockList, isNotNull);
// Scroll down // Scroll down
......
...@@ -22,12 +22,12 @@ void main() { ...@@ -22,12 +22,12 @@ void main() {
test('measure', () async { test('measure', () async {
Timeline timeline = await driver.traceAction(() async { Timeline timeline = await driver.traceAction(() async {
// Find the scrollable stock list // Find the scrollable stock list
ObjectRef stockList = await driver.findByValueKey('Gallery List'); SerializableFinder stockList = find.byValueKey('Gallery List');
expect(stockList, isNotNull); expect(stockList, isNotNull);
await driver.tap(await driver.findByText('Demos')); await driver.tap(find.text('Demos'));
await driver.tap(await driver.findByText('Components')); await driver.tap(find.text('Components'));
await driver.tap(await driver.findByText('Style')); await driver.tap(find.text('Style'));
// TODO(eseidel): These are very artifical scrolls, we should use better // TODO(eseidel): These are very artifical scrolls, we should use better
// https://github.com/flutter/flutter/issues/3316 // https://github.com/flutter/flutter/issues/3316
......
...@@ -23,7 +23,7 @@ void main() { ...@@ -23,7 +23,7 @@ void main() {
test('measure', () async { test('measure', () async {
Timeline timeline = await driver.traceAction(() async { Timeline timeline = await driver.traceAction(() async {
// Find the scrollable stock list // Find the scrollable stock list
ObjectRef stockList = await driver.findByValueKey('stock-list'); SerializableFinder stockList = find.byValueKey('stock-list');
expect(stockList, isNotNull); expect(stockList, isNotNull);
// Scroll down // Scroll down
......
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
library flutter_driver; library flutter_driver;
export 'src/driver.dart' show export 'src/driver.dart' show
find,
CommonFinders,
EvaluatorFunction,
FlutterDriver; FlutterDriver;
export 'src/error.dart' show export 'src/error.dart' show
...@@ -21,7 +24,7 @@ export 'src/error.dart' show ...@@ -21,7 +24,7 @@ export 'src/error.dart' show
flutterDriverLog; flutterDriverLog;
export 'src/find.dart' show export 'src/find.dart' show
ObjectRef, SerializableFinder,
GetTextResult; GetTextResult;
export 'src/health.dart' show export 'src/health.dart' show
...@@ -31,7 +34,6 @@ export 'src/health.dart' show ...@@ -31,7 +34,6 @@ export 'src/health.dart' show
export 'src/message.dart' show export 'src/message.dart' show
Message, Message,
Command, Command,
ObjectRef,
CommandWithTarget, CommandWithTarget,
Result; Result;
......
...@@ -20,6 +20,14 @@ import 'timeline.dart'; ...@@ -20,6 +20,14 @@ import 'timeline.dart';
final Logger _log = new Logger('FlutterDriver'); 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. /// Computes a value.
/// ///
/// If computation is asynchronous, the function may return a [Future]. /// If computation is asynchronous, the function may return a [Future].
...@@ -162,14 +170,15 @@ class FlutterDriver { ...@@ -162,14 +170,15 @@ class FlutterDriver {
Future<Map<String, dynamic>> _sendCommand(Command command) async { Future<Map<String, dynamic>> _sendCommand(Command command) async {
Map<String, String> parameters = <String, String>{'command': command.kind} Map<String, String> parameters = <String, String>{'command': command.kind}
..addAll(command.serialize()); ..addAll(command.serialize());
return _appIsolate.invokeExtension(_kFlutterExtensionMethod, parameters) try {
.then((Map<String, dynamic> result) => result, onError: (dynamic error, dynamic stackTrace) { return await _appIsolate.invokeExtension(_kFlutterExtensionMethod, parameters);
} catch (error, stackTrace) {
throw new DriverError( throw new DriverError(
'Failed to fulfill ${command.runtimeType} due to remote error', 'Failed to fulfill ${command.runtimeType} due to remote error',
error, error,
stackTrace stackTrace
); );
}); }
} }
/// Checks the status of the Flutter Driver extension. /// Checks the status of the Flutter Driver extension.
...@@ -177,23 +186,14 @@ class FlutterDriver { ...@@ -177,23 +186,14 @@ class FlutterDriver {
return Health.fromJson(await _sendCommand(new GetHealth())); return Health.fromJson(await _sendCommand(new GetHealth()));
} }
/// Finds the UI element with the given [key]. /// Taps at the center of the widget located by [finder].
Future<ObjectRef> findByValueKey(dynamic key) async { Future<Null> tap(SerializableFinder finder) async {
return ObjectRef.fromJson(await _sendCommand(new Find(new ByValueKey(key)))); return await _sendCommand(new Tap(finder)).then((Map<String, dynamic> _) => null);
}
/// Finds the UI element for the tooltip with the given [message].
Future<ObjectRef> findByTooltipMessage(String message) async {
return ObjectRef.fromJson(await _sendCommand(new Find(new ByTooltipMessage(message))));
} }
/// Finds the text element with the given [text]. /// Whether at least one widget identified by [finder] exists on the UI.
Future<ObjectRef> findByText(String text) async { Future<bool> exists(SerializableFinder finder) async {
return ObjectRef.fromJson(await _sendCommand(new Find(new ByText(text)))); return await _sendCommand(new Exists(finder)).then((Map<String, dynamic> _) => null);
}
Future<Null> tap(ObjectRef ref) async {
return await _sendCommand(new Tap(ref)).then((Map<String, dynamic> _) => null);
} }
/// Tell the driver to perform a scrolling action. /// Tell the driver to perform a scrolling action.
...@@ -209,13 +209,13 @@ class FlutterDriver { ...@@ -209,13 +209,13 @@ class FlutterDriver {
/// ///
/// The move events are generated at a given [frequency] in Hz (or events per /// The move events are generated at a given [frequency] in Hz (or events per
/// second). It defaults to 60Hz. /// second). It defaults to 60Hz.
Future<Null> scroll(ObjectRef ref, double dx, double dy, Duration duration, {int frequency: 60}) async { Future<Null> scroll(SerializableFinder finder, double dx, double dy, Duration duration, {int frequency: 60}) async {
return await _sendCommand(new Scroll(ref, dx, dy, duration, frequency)).then((Map<String, dynamic> _) => null); return await _sendCommand(new Scroll(finder, dx, dy, duration, frequency)).then((Map<String, dynamic> _) => null);
} }
Future<String> getText(ObjectRef ref) async { /// Returns the text in the `Text` widget located by [finder].
GetTextResult result = GetTextResult.fromJson(await _sendCommand(new GetText(ref))); Future<String> getText(SerializableFinder finder) async {
return result.text; return GetTextResult.fromJson(await _sendCommand(new GetText(finder))).text;
} }
/// Starts recording performance traces. /// Starts recording performance traces.
...@@ -358,3 +358,17 @@ Future<VMServiceClientConnection> _waitAndConnect(String url) async { ...@@ -358,3 +358,17 @@ Future<VMServiceClientConnection> _waitAndConnect(String url) async {
return attemptConnection(); 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);
}
...@@ -47,34 +47,39 @@ typedef Future<Result> CommandHandlerCallback(Command c); ...@@ -47,34 +47,39 @@ typedef Future<Result> CommandHandlerCallback(Command c);
/// Deserializes JSON map to a command object. /// Deserializes JSON map to a command object.
typedef Command CommandDeserializerCallback(Map<String, String> params); typedef Command CommandDeserializerCallback(Map<String, String> params);
/// Runs the finder and returns the [Element] found, or `null`.
typedef Future<Element> FinderCallback(SerializableFinder finder);
class FlutterDriverExtension { class FlutterDriverExtension {
static final Logger _log = new Logger('FlutterDriverExtension'); static final Logger _log = new Logger('FlutterDriverExtension');
FlutterDriverExtension() { FlutterDriverExtension() {
_commandHandlers = { _commandHandlers = <String, CommandHandlerCallback>{
'get_health': getHealth, 'get_health': getHealth,
'find': find,
'tap': tap, 'tap': tap,
'get_text': getText, 'get_text': getText,
'scroll': scroll, 'scroll': scroll,
}; };
_commandDeserializers = { _commandDeserializers = <String, CommandDeserializerCallback>{
'get_health': GetHealth.deserialize, 'get_health': GetHealth.deserialize,
'find': Find.deserialize,
'tap': Tap.deserialize, 'tap': Tap.deserialize,
'get_text': GetText.deserialize, 'get_text': GetText.deserialize,
'scroll': Scroll.deserialize, 'scroll': Scroll.deserialize,
}; };
_finders = <String, FinderCallback>{
'ByValueKey': _findByValueKey,
'ByTooltipMessage': _findByTooltipMessage,
'ByText': _findByText,
};
} }
final Instrumentation prober = new Instrumentation(); final Instrumentation prober = new Instrumentation();
Map<String, CommandHandlerCallback> _commandHandlers = Map<String, CommandHandlerCallback> _commandHandlers;
<String, CommandHandlerCallback>{}; Map<String, CommandDeserializerCallback> _commandDeserializers;
Map<String, FinderCallback> _finders;
Map<String, CommandDeserializerCallback> _commandDeserializers =
<String, CommandDeserializerCallback>{};
Future<ServiceExtensionResponse> call(Map<String, String> params) async { Future<ServiceExtensionResponse> call(Map<String, String> params) async {
try { try {
...@@ -107,36 +112,18 @@ class FlutterDriverExtension { ...@@ -107,36 +112,18 @@ class FlutterDriverExtension {
Future<Health> getHealth(GetHealth command) async => new Health(HealthStatus.ok); Future<Health> getHealth(GetHealth command) async => new Health(HealthStatus.ok);
Future<ObjectRef> find(Find command) async { /// Runs object [finder] repeatedly until it finds an [Element].
SearchSpecification searchSpec = command.searchSpec; Future<Element> _waitForElement(String descriptionGetter(), Element locator()) {
switch(searchSpec.runtimeType) { return retry(locator, _kDefaultTimeout, _kDefaultPauseBetweenRetries, predicate: (dynamic object) {
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<ObjectRef> _waitForObject(String descriptionGetter(), Object locator()) async {
Object object = await retry(locator, _kDefaultTimeout, _kDefaultPauseBetweenRetries, predicate: (Object object) {
return object != null; return object != null;
}).catchError((Object error, Object stackTrace) { }).catchError((Object error, Object stackTrace) {
_log.warning('Timed out waiting for ${descriptionGetter()}'); _log.warning('Timed out waiting for ${descriptionGetter()}');
return null; return null;
}); });
ObjectRef elemRef = object != null
? new ObjectRef(_registerObject(object))
: new ObjectRef.notFound();
return new Future<ObjectRef>.value(elemRef);
} }
Future<ObjectRef> findByValueKey(ByValueKey byKey) async { Future<Element> _findByValueKey(ByValueKey byKey) async {
return _waitForObject( return _waitForElement(
() => 'element with key "${byKey.keyValue}" of type ${byKey.keyValueType}', () => 'element with key "${byKey.keyValue}" of type ${byKey.keyValueType}',
() { () {
return prober.findElementByKey(new ValueKey<dynamic>(byKey.keyValue)); return prober.findElementByKey(new ValueKey<dynamic>(byKey.keyValue));
...@@ -144,8 +131,8 @@ class FlutterDriverExtension { ...@@ -144,8 +131,8 @@ class FlutterDriverExtension {
); );
} }
Future<ObjectRef> findByTooltipMessage(ByTooltipMessage byTooltipMessage) async { Future<Element> _findByTooltipMessage(ByTooltipMessage byTooltipMessage) async {
return _waitForObject( return _waitForElement(
() => 'tooltip with message "${byTooltipMessage.text}" on it', () => 'tooltip with message "${byTooltipMessage.text}" on it',
() { () {
return prober.findElement((Element element) { return prober.findElement((Element element) {
...@@ -160,22 +147,31 @@ class FlutterDriverExtension { ...@@ -160,22 +147,31 @@ class FlutterDriverExtension {
); );
} }
Future<ObjectRef> findByText(ByText byText) async { Future<Element> _findByText(ByText byText) async {
return await _waitForObject( return await _waitForElement(
() => 'text "${byText.text}"', () => 'text "${byText.text}"',
() { () {
return prober.findText(byText.text); return prober.findText(byText.text);
}); });
} }
Future<Element> _runFinder(SerializableFinder finder) {
FinderCallback cb = _finders[finder.finderType];
if (cb == null)
throw 'Unsupported finder type: ${finder.finderType}';
return cb(finder);
}
Future<TapResult> tap(Tap command) async { Future<TapResult> tap(Tap command) async {
Element target = await _dereferenceOrDie(command.targetRef); Element target = await _runFinder(command.finder);
prober.tap(target); prober.tap(target);
return new TapResult(); return new TapResult();
} }
Future<ScrollResult> scroll(Scroll command) async { Future<ScrollResult> 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; final int totalMoves = command.duration.inMicroseconds * command.frequency ~/ Duration.MICROSECONDS_PER_SECOND;
Offset delta = new Offset(command.dx, command.dy) / totalMoves.toDouble(); Offset delta = new Offset(command.dx, command.dy) / totalMoves.toDouble();
Duration pause = command.duration ~/ totalMoves; Duration pause = command.duration ~/ totalMoves;
...@@ -198,30 +194,9 @@ class FlutterDriverExtension { ...@@ -198,30 +194,9 @@ class FlutterDriverExtension {
} }
Future<GetTextResult> getText(GetText command) async { Future<GetTextResult> getText(GetText command) async {
Element target = await _dereferenceOrDie(command.targetRef); Element target = await _runFinder(command.finder);
// TODO(yjbanov): support more ways to read text // TODO(yjbanov): support more ways to read text
Text text = target.widget; Text text = target.widget;
return new GetTextResult(text.data); return new GetTextResult(text.data);
} }
int _refCounter = 1;
final Map<String, Object> _objectRefs = <String, Object>{};
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<dynamic> _dereferenceOrDie(String reference) {
Element object = _dereference(reference);
if (object == null)
return new Future<String>.error('Object reference not found ($reference).');
return new Future<Element>.value(object);
}
} }
...@@ -11,46 +11,86 @@ DriverError _createInvalidKeyValueTypeError(String invalidType) { ...@@ -11,46 +11,86 @@ DriverError _createInvalidKeyValueTypeError(String invalidType) {
return new DriverError('Unsupported key value type $invalidType. Flutter Driver only supports ${_supportedKeyValueTypes.join(", ")}'); return new DriverError('Unsupported key value type $invalidType. Flutter Driver only supports ${_supportedKeyValueTypes.join(", ")}');
} }
/// Command to find an element. /// A command aimed at an object to be located by [finder].
class Find extends Command { ///
/// 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<String, String> toJson() => super.toJson()..addAll({
/// 'foo': this.foo,
/// });
@override @override
final String kind = 'find'; Map<String, String> serialize() => finder.serialize();
}
/// Checks if the widget identified by the given finder exists.
class Exists extends CommandWithTarget {
@override
final String kind = 'exists';
Find(this.searchSpec); Exists(SerializableFinder finder) : super(finder);
final SearchSpecification searchSpec; static Exists deserialize(Map<String, String> json) {
return new Exists(SerializableFinder.deserialize(json));
}
@override @override
Map<String, String> serialize() => searchSpec.serialize(); Map<String, String> serialize() => super.serialize();
}
static Find deserialize(Map<String, String> json) { class ExistsResult extends Result {
return new Find(SearchSpecification.deserialize(json)); ExistsResult(this.exists);
static ExistsResult fromJson(Map<String, dynamic> json) {
return new ExistsResult(json['exists']);
} }
/// Whether the widget was found on the UI or not.
final bool exists;
@override
Map<String, dynamic> toJson() => {
'exists': exists,
};
} }
/// Describes how to the driver should search for elements. /// Describes how to the driver should search for elements.
abstract class SearchSpecification { abstract class SerializableFinder {
String get searchSpecType; String get finderType;
static SearchSpecification deserialize(Map<String, String> json) { static SerializableFinder deserialize(Map<String, String> json) {
String searchSpecType = json['searchSpecType']; String finderType = json['finderType'];
switch(searchSpecType) { switch(finderType) {
case 'ByValueKey': return ByValueKey.deserialize(json); case 'ByValueKey': return ByValueKey.deserialize(json);
case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json); case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json);
case 'ByText': return ByText.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<String, String> serialize() => { Map<String, String> serialize() => {
'searchSpecType': searchSpecType, 'finderType': finderType,
}; };
} }
/// Tells [Find] to search by tooltip text. /// Finds widgets by tooltip text.
class ByTooltipMessage extends SearchSpecification { class ByTooltipMessage extends SerializableFinder {
@override @override
final String searchSpecType = 'ByTooltipMessage'; final String finderType = 'ByTooltipMessage';
ByTooltipMessage(this.text); ByTooltipMessage(this.text);
...@@ -67,10 +107,10 @@ class ByTooltipMessage extends SearchSpecification { ...@@ -67,10 +107,10 @@ class ByTooltipMessage extends SearchSpecification {
} }
} }
/// Tells [Find] to search for `Text` widget by text. /// Finds widgets by [text] inside a `Text` widget.
class ByText extends SearchSpecification { class ByText extends SerializableFinder {
@override @override
final String searchSpecType = 'ByText'; final String finderType = 'ByText';
ByText(this.text); ByText(this.text);
...@@ -86,10 +126,10 @@ class ByText extends SearchSpecification { ...@@ -86,10 +126,10 @@ class ByText extends SearchSpecification {
} }
} }
/// Tells [Find] to search by `ValueKey`. /// Finds widgets by `ValueKey`.
class ByValueKey extends SearchSpecification { class ByValueKey extends SerializableFinder {
@override @override
final String searchSpecType = 'ByValueKey'; final String finderType = 'ByValueKey';
ByValueKey(dynamic keyValue) ByValueKey(dynamic keyValue)
: this.keyValue = keyValue, : this.keyValue = keyValue,
...@@ -132,14 +172,14 @@ class ByValueKey extends SearchSpecification { ...@@ -132,14 +172,14 @@ class ByValueKey extends SearchSpecification {
/// Command to read the text from a given element. /// Command to read the text from a given element.
class GetText extends CommandWithTarget { class GetText extends CommandWithTarget {
/// [targetRef] identifies an element that contains a piece of text. /// [finder] looks for an element that contains a piece of text.
GetText(ObjectRef targetRef) : super(targetRef); GetText(SerializableFinder finder) : super(finder);
@override @override
final String kind = 'get_text'; final String kind = 'get_text';
static GetText deserialize(Map<String, String> json) { static GetText deserialize(Map<String, String> json) {
return new GetText(new ObjectRef(json['targetRef'])); return new GetText(SerializableFinder.deserialize(json));
} }
@override @override
......
...@@ -3,15 +3,16 @@ ...@@ -3,15 +3,16 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'message.dart'; import 'message.dart';
import 'find.dart';
class Tap extends CommandWithTarget { class Tap extends CommandWithTarget {
@override @override
final String kind = 'tap'; final String kind = 'tap';
Tap(ObjectRef targetRef) : super(targetRef); Tap(SerializableFinder finder) : super(finder);
static Tap deserialize(Map<String, String> json) { static Tap deserialize(Map<String, String> json) {
return new Tap(new ObjectRef(json['targetRef'])); return new Tap(SerializableFinder.deserialize(json));
} }
@override @override
...@@ -34,16 +35,16 @@ class Scroll extends CommandWithTarget { ...@@ -34,16 +35,16 @@ class Scroll extends CommandWithTarget {
final String kind = 'scroll'; final String kind = 'scroll';
Scroll( Scroll(
ObjectRef targetRef, SerializableFinder finder,
this.dx, this.dx,
this.dy, this.dy,
this.duration, this.duration,
this.frequency this.frequency
) : super(targetRef); ) : super(finder);
static Scroll deserialize(Map<String, dynamic> json) { static Scroll deserialize(Map<String, dynamic> json) {
return new Scroll( return new Scroll(
new ObjectRef(json['targetRef']), SerializableFinder.deserialize(json),
double.parse(json['dx']), double.parse(json['dx']),
double.parse(json['dy']), double.parse(json['dy']),
new Duration(microseconds: int.parse(json['duration'])), new Duration(microseconds: int.parse(json['duration'])),
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'error.dart';
/// An object sent from the Flutter Driver to a Flutter application to instruct /// An object sent from the Flutter Driver to a Flutter application to instruct
/// the application to perform a task. /// the application to perform a task.
abstract class Command { abstract class Command {
...@@ -20,58 +18,3 @@ abstract class Result { // ignore: one_member_abstracts ...@@ -20,58 +18,3 @@ abstract class Result { // ignore: one_member_abstracts
/// Serializes this message to a JSON map. /// Serializes this message to a JSON map.
Map<String, dynamic> toJson(); Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, String> toJson() => super.toJson()..addAll({
/// 'foo': this.foo,
/// });
@override
Map<String, String> serialize() => <String, String>{
'targetRef': targetRef,
};
}
...@@ -7,7 +7,6 @@ import 'dart:async'; ...@@ -7,7 +7,6 @@ import 'dart:async';
import 'package:flutter_driver/src/driver.dart'; import 'package:flutter_driver/src/driver.dart';
import 'package:flutter_driver/src/error.dart'; import 'package:flutter_driver/src/error.dart';
import 'package:flutter_driver/src/health.dart'; import 'package:flutter_driver/src/health.dart';
import 'package:flutter_driver/src/message.dart';
import 'package:flutter_driver/src/timeline.dart'; import 'package:flutter_driver/src/timeline.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
...@@ -120,27 +119,23 @@ void main() { ...@@ -120,27 +119,23 @@ void main() {
await driver.close(); await driver.close();
}); });
group('findByValueKey', () { group('ByValueKey', () {
test('restricts value types', () async { test('restricts value types', () async {
expect(driver.findByValueKey(null), expect(() => find.byValueKey(null),
throwsA(new isInstanceOf<DriverError>())); throwsA(new isInstanceOf<DriverError>()));
}); });
test('finds by ValueKey', () async { test('finds by ValueKey', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], { expect(i.positionalArguments[1], {
'command': 'find', 'command': 'tap',
'searchSpecType': 'ByValueKey', 'finderType': 'ByValueKey',
'keyValueString': 'foo', 'keyValueString': 'foo',
'keyValueType': 'String' 'keyValueType': 'String'
}); });
return new Future<Map<String, dynamic>>.value(<String, dynamic>{ return new Future<Null>.value();
'objectReferenceKey': '123',
});
}); });
ObjectRef result = await driver.findByValueKey('foo'); await driver.tap(find.byValueKey('foo'));
expect(result, isNotNull);
expect(result.objectReferenceKey, '123');
}); });
}); });
...@@ -149,20 +144,16 @@ void main() { ...@@ -149,20 +144,16 @@ void main() {
expect(driver.tap(null), throwsA(new isInstanceOf<DriverError>())); expect(driver.tap(null), throwsA(new isInstanceOf<DriverError>()));
}); });
test('requires a valid target reference', () async {
expect(driver.tap(new ObjectRef.notFound()),
throwsA(new isInstanceOf<DriverError>()));
});
test('sends the tap command', () async { test('sends the tap command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{ expect(i.positionalArguments[1], <String, dynamic>{
'command': 'tap', 'command': 'tap',
'targetRef': '123' 'finderType': 'ByText',
'text': 'foo',
}); });
return new Future<Map<String, dynamic>>.value(); return new Future<Map<String, dynamic>>.value();
}); });
await driver.tap(new ObjectRef('123')); await driver.tap(find.text('foo'));
}); });
}); });
...@@ -171,22 +162,19 @@ void main() { ...@@ -171,22 +162,19 @@ void main() {
expect(driver.getText(null), throwsA(new isInstanceOf<DriverError>())); expect(driver.getText(null), throwsA(new isInstanceOf<DriverError>()));
}); });
test('requires a valid target reference', () async {
expect(driver.getText(new ObjectRef.notFound()),
throwsA(new isInstanceOf<DriverError>()));
});
test('sends the getText command', () async { test('sends the getText command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) { when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{ expect(i.positionalArguments[1], <String, dynamic>{
'command': 'get_text', 'command': 'get_text',
'targetRef': '123' 'finderType': 'ByValueKey',
'keyValueString': '123',
'keyValueType': 'int'
}); });
return new Future<Map<String, dynamic>>.value({ return new Future<Map<String, dynamic>>.value({
'text': 'hello' 'text': 'hello'
}); });
}); });
String result = await driver.getText(new ObjectRef('123')); String result = await driver.getText(find.byValueKey(123));
expect(result, 'hello'); expect(result, 'hello');
}); });
}); });
......
...@@ -23,14 +23,14 @@ void main() { ...@@ -23,14 +23,14 @@ void main() {
test('tap on the floating action button; verify counter', () async { test('tap on the floating action button; verify counter', () async {
// Find floating action button (fab) to tap on // Find floating action button (fab) to tap on
ObjectRef fab = await driver.findByTooltipMessage('Increment'); SerializableFinder fab = find.byTooltip('Increment');
expect(fab, isNotNull); expect(await driver.exists(fab), isTrue);
// Tap on the fab // Tap on the fab
await driver.tap(fab); await driver.tap(fab);
// Wait for text to change to the desired value // 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);
}); });
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment