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

This commit is contained in:
Yegor 2016-04-15 16:57:35 -07:00
parent a91bc0ba9c
commit 9ce995f65e
11 changed files with 182 additions and 219 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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<Map<String, dynamic>> _sendCommand(Command command) async {
Map<String, String> parameters = <String, String>{'command': command.kind}
..addAll(command.serialize());
return _appIsolate.invokeExtension(_kFlutterExtensionMethod, parameters)
.then((Map<String, dynamic> 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<ObjectRef> 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<Null> tap(SerializableFinder finder) async {
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].
Future<ObjectRef> findByText(String text) async {
return ObjectRef.fromJson(await _sendCommand(new Find(new ByText(text))));
}
Future<Null> tap(ObjectRef ref) async {
return await _sendCommand(new Tap(ref)).then((Map<String, dynamic> _) => null);
/// Whether at least one widget identified by [finder] exists on the UI.
Future<bool> exists(SerializableFinder finder) async {
return await _sendCommand(new Exists(finder)).then((Map<String, dynamic> _) => 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<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((Map<String, dynamic> _) => null);
Future<Null> 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<String, dynamic> _) => null);
}
Future<String> 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<String> getText(SerializableFinder finder) async {
return GetTextResult.fromJson(await _sendCommand(new GetText(finder))).text;
}
/// Starts recording performance traces.
@ -358,3 +358,17 @@ Future<VMServiceClientConnection> _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);
}

View file

@ -47,34 +47,39 @@ typedef Future<Result> CommandHandlerCallback(Command c);
/// Deserializes JSON map to a command object.
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 {
static final Logger _log = new Logger('FlutterDriverExtension');
FlutterDriverExtension() {
_commandHandlers = {
_commandHandlers = <String, CommandHandlerCallback>{
'get_health': getHealth,
'find': find,
'tap': tap,
'get_text': getText,
'scroll': scroll,
};
_commandDeserializers = {
_commandDeserializers = <String, CommandDeserializerCallback>{
'get_health': GetHealth.deserialize,
'find': Find.deserialize,
'tap': Tap.deserialize,
'get_text': GetText.deserialize,
'scroll': Scroll.deserialize,
};
_finders = <String, FinderCallback>{
'ByValueKey': _findByValueKey,
'ByTooltipMessage': _findByTooltipMessage,
'ByText': _findByText,
};
}
final Instrumentation prober = new Instrumentation();
Map<String, CommandHandlerCallback> _commandHandlers =
<String, CommandHandlerCallback>{};
Map<String, CommandDeserializerCallback> _commandDeserializers =
<String, CommandDeserializerCallback>{};
Map<String, CommandHandlerCallback> _commandHandlers;
Map<String, CommandDeserializerCallback> _commandDeserializers;
Map<String, FinderCallback> _finders;
Future<ServiceExtensionResponse> call(Map<String, String> params) async {
try {
@ -107,36 +112,18 @@ class FlutterDriverExtension {
Future<Health> getHealth(GetHealth command) async => new Health(HealthStatus.ok);
Future<ObjectRef> 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<ObjectRef> _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<Element> _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<ObjectRef>.value(elemRef);
}
Future<ObjectRef> findByValueKey(ByValueKey byKey) async {
return _waitForObject(
Future<Element> _findByValueKey(ByValueKey byKey) async {
return _waitForElement(
() => 'element with key "${byKey.keyValue}" of type ${byKey.keyValueType}',
() {
return prober.findElementByKey(new ValueKey<dynamic>(byKey.keyValue));
@ -144,8 +131,8 @@ class FlutterDriverExtension {
);
}
Future<ObjectRef> findByTooltipMessage(ByTooltipMessage byTooltipMessage) async {
return _waitForObject(
Future<Element> _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<ObjectRef> findByText(ByText byText) async {
return await _waitForObject(
Future<Element> _findByText(ByText byText) async {
return await _waitForElement(
() => 'text "${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 {
Element target = await _dereferenceOrDie(command.targetRef);
Element target = await _runFinder(command.finder);
prober.tap(target);
return new TapResult();
}
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;
Offset delta = new Offset(command.dx, command.dy) / totalMoves.toDouble();
Duration pause = command.duration ~/ totalMoves;
@ -198,30 +194,9 @@ class FlutterDriverExtension {
}
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
Text text = target.widget;
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);
}
}

View file

@ -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<String, String> serialize() => searchSpec.serialize();
static Find deserialize(Map<String, String> 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<String, String> toJson() => super.toJson()..addAll({
/// 'foo': this.foo,
/// });
@override
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';
Exists(SerializableFinder finder) : super(finder);
static Exists deserialize(Map<String, String> json) {
return new Exists(SerializableFinder.deserialize(json));
}
@override
Map<String, String> serialize() => super.serialize();
}
class ExistsResult extends Result {
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.
abstract class SearchSpecification {
String get searchSpecType;
abstract class SerializableFinder {
String get finderType;
static SearchSpecification deserialize(Map<String, String> json) {
String searchSpecType = json['searchSpecType'];
switch(searchSpecType) {
static SerializableFinder deserialize(Map<String, String> 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<String, String> 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<String, String> json) {
return new GetText(new ObjectRef(json['targetRef']));
return new GetText(SerializableFinder.deserialize(json));
}
@override

View file

@ -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<String, String> 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<String, dynamic> 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'])),

View file

@ -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<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,
};
}

View file

@ -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<DriverError>()));
});
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<Map<String, dynamic>>.value(<String, dynamic>{
'objectReferenceKey': '123',
});
return new Future<Null>.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<DriverError>()));
});
test('requires a valid target reference', () async {
expect(driver.tap(new ObjectRef.notFound()),
throwsA(new isInstanceOf<DriverError>()));
});
test('sends the tap command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{
'command': 'tap',
'targetRef': '123'
'finderType': 'ByText',
'text': 'foo',
});
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() {
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 {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{
'command': 'get_text',
'targetRef': '123'
'finderType': 'ByValueKey',
'keyValueString': '123',
'keyValueType': 'int'
});
return new Future<Map<String, dynamic>>.value({
'text': 'hello'
});
});
String result = await driver.getText(new ObjectRef('123'));
String result = await driver.getText(find.byValueKey(123));
expect(result, 'hello');
});
});

View file

@ -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);
});
});
}