Commit 6d35481c authored by Yegor Jbanov's avatar Yegor Jbanov

add smoke driver test; find by tooltip; retry predicate

parent 6a1f47a5
......@@ -151,8 +151,19 @@ 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 FindByValueKey(key)));
return ObjectRef.fromJson(await _sendCommand(new Find(new ByValueKey(key))));
}
/// 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 {
......
......@@ -6,7 +6,8 @@
///
/// In Dart enum names are prefixed with enum class name. For example, for
/// `enum Vote { yea, nay }`, `Vote.yea.toString()` produces `"Vote.yea"`
/// rather than just `"yea"` - the simple name.
/// rather than just `"yea"` - the simple name. This class provides methods for
/// getting and looking up by simple names.
///
/// Example:
///
......
......@@ -15,7 +15,13 @@ class DriverError extends Error {
final dynamic originalError;
final dynamic originalStackTrace;
String toString() => 'DriverError: $message';
String toString() {
return '''DriverError: $message
Original error: $originalError
Original stack trace:
$originalStackTrace
''';
}
}
// Whether someone redirected the log messages somewhere.
......
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/src/instrumentation.dart';
......@@ -50,14 +51,14 @@ class FlutterDriverExtension {
FlutterDriverExtension() {
_commandHandlers = {
'get_health': getHealth,
'find_by_value_key': findByValueKey,
'find': find,
'tap': tap,
'get_text': getText,
};
_commandDeserializers = {
'get_health': GetHealth.fromJson,
'find_by_value_key': FindByValueKey.fromJson,
'find': Find.fromJson,
'tap': Tap.fromJson,
'get_text': GetText.fromJson,
};
......@@ -72,41 +73,95 @@ class FlutterDriverExtension {
<String, CommandDeserializerCallback>{};
Future<ServiceExtensionResponse> call(Map<String, String> params) async {
String commandKind = params['kind'];
CommandHandlerCallback commandHandler = _commandHandlers[commandKind];
CommandDeserializerCallback commandDeserializer =
_commandDeserializers[commandKind];
if (commandHandler == null || commandDeserializer == null) {
return new ServiceExtensionResponse.error(
ServiceExtensionResponse.kInvalidParams,
'Extension $_extensionMethod does not support command $commandKind'
);
try {
String commandKind = params['kind'];
CommandHandlerCallback commandHandler = _commandHandlers[commandKind];
CommandDeserializerCallback commandDeserializer =
_commandDeserializers[commandKind];
if (commandHandler == null || commandDeserializer == null) {
return new ServiceExtensionResponse.error(
ServiceExtensionResponse.kInvalidParams,
'Extension $_extensionMethod does not support command $commandKind'
);
}
Command command = commandDeserializer(params);
return commandHandler(command).then((Result result) {
return new ServiceExtensionResponse.result(JSON.encode(result.toJson()));
}, onError: (e, s) {
_log.warning('$e:\n$s');
return new ServiceExtensionResponse.error(
ServiceExtensionResponse.kExtensionError, '$e');
});
} catch(error, stackTrace) {
_log.warning('Uncaught extension error: $error\n$stackTrace');
}
Command command = commandDeserializer(params);
return commandHandler(command).then((Result result) {
return new ServiceExtensionResponse.result(JSON.encode(result.toJson()));
}, onError: (e, s) {
_log.warning('$e:\n$s');
return new ServiceExtensionResponse.error(
ServiceExtensionResponse.kExtensionError, '$e');
});
}
Future<Health> getHealth(GetHealth command) async => new Health(HealthStatus.ok);
Future<ObjectRef> findByValueKey(FindByValueKey command) async {
Element elem = await retry(() {
return prober.findElementByKey(new ValueKey<dynamic>(command.keyValue));
}, _kDefaultTimeout, _kDefaultPauseBetweenRetries);
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}');
}
ObjectRef elemRef = elem != null
? new ObjectRef(_registerObject(elem))
/// 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) {
return object != null;
}).catchError((dynamic error, 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<ObjectRef> findByValueKey(ByValueKey byKey) async {
return _waitForObject(
() => 'element with key "${byKey.keyValue}" of type ${byKey.keyValueType}',
() {
return prober.findElementByKey(new ValueKey<dynamic>(byKey.keyValue));
}
);
}
Future<ObjectRef> findByTooltipMessage(ByTooltipMessage byTooltipMessage) async {
return _waitForObject(
() => 'tooltip with message "${byTooltipMessage.text}" on it',
() {
return prober.findElement((Element element) {
Widget widget = element.widget;
if (widget is Tooltip)
return widget.message == byTooltipMessage.text;
return false;
});
}
);
}
Future<ObjectRef> findByText(ByText byText) async {
return await _waitForObject(
() => 'text "${byText.text}"',
() {
return prober.findText(byText.text);
});
}
Future<TapResult> tap(Tap command) async {
Element target = await _dereferenceOrDie(command.targetRef);
prober.tap(target);
......
......@@ -7,11 +7,84 @@ import 'message.dart';
const List<Type> _supportedKeyValueTypes = const <Type>[String, int];
/// Command to find an element by a value key.
class FindByValueKey extends Command {
final String kind = 'find_by_value_key';
/// Command to find an element.
class Find extends Command {
final String kind = 'find';
FindByValueKey(dynamic keyValue)
Find(this.searchSpec);
final SearchSpecification searchSpec;
Map<String, dynamic> toJson() => searchSpec.toJson();
static Find fromJson(Map<String, dynamic> json) {
return new Find(SearchSpecification.fromJson(json));
}
static _throwInvalidKeyValueType(String invalidType) {
throw new DriverError('Unsupported key value type $invalidType. Flutter Driver only supports ${_supportedKeyValueTypes.join(", ")}');
}
}
/// Describes how to the driver should search for elements.
abstract class SearchSpecification extends Message {
String get searchSpecType;
static SearchSpecification fromJson(Map<String, dynamic> json) {
String searchSpecType = json['searchSpecType'];
switch(searchSpecType) {
case 'ByValueKey': return ByValueKey.fromJson(json);
case 'ByTooltipMessage': return ByTooltipMessage.fromJson(json);
case 'ByText': return ByText.fromJson(json);
}
throw new DriverError('Unsupported search specification type $searchSpecType');
}
Map<String, dynamic> toJson() => {
'searchSpecType': searchSpecType,
};
}
/// Tells [Find] to search by tooltip text.
class ByTooltipMessage extends SearchSpecification {
final String searchSpecType = 'ByTooltipMessage';
ByTooltipMessage(this.text);
/// Tooltip message text.
final String text;
Map<String, dynamic> toJson() => super.toJson()..addAll({
'text': text,
});
static ByTooltipMessage fromJson(Map<String, dynamic> json) {
return new ByTooltipMessage(json['text']);
}
}
/// Tells [Find] to search for `Text` widget by text.
class ByText extends SearchSpecification {
final String searchSpecType = 'ByText';
ByText(this.text);
final String text;
Map<String, dynamic> toJson() => super.toJson()..addAll({
'text': text,
});
static ByText fromJson(Map<String, dynamic> json) {
return new ByText(json['text']);
}
}
/// Tells [Find] to search by `ValueKey`.
class ByValueKey extends SearchSpecification {
final String searchSpecType = 'ByValueKey';
ByValueKey(dynamic keyValue)
: this.keyValue = keyValue,
this.keyValueString = '$keyValue',
this.keyValueType = '${keyValue.runtimeType}' {
......@@ -30,19 +103,19 @@ class FindByValueKey extends Command {
/// May be one of "String", "int". The list of supported types may change.
final String keyValueType;
Map<String, dynamic> toJson() => {
Map<String, dynamic> toJson() => super.toJson()..addAll({
'keyValueString': keyValueString,
'keyValueType': keyValueType,
};
});
static FindByValueKey fromJson(Map<String, dynamic> json) {
static ByValueKey fromJson(Map<String, dynamic> json) {
String keyValueString = json['keyValueString'];
String keyValueType = json['keyValueType'];
switch(keyValueType) {
case 'int':
return new FindByValueKey(int.parse(keyValueString));
return new ByValueKey(int.parse(keyValueString));
case 'String':
return new FindByValueKey(keyValueString);
return new ByValueKey(keyValueString);
default:
return _throwInvalidKeyValueType(keyValueType);
}
......
......@@ -8,13 +8,17 @@ import 'dart:async';
/// that evaluates to the result.
typedef dynamic Action();
/// Determines if [value] is acceptable. For good style an implementation should
/// be idempotent.
typedef bool Predicate(dynamic value);
/// Performs [action] repeatedly until it either succeeds or [timeout] limit is
/// reached.
///
/// When the retry time out, the last seen error and stack trace are returned in
/// an error [Future].
Future<dynamic> retry(Action action, Duration timeout,
Duration pauseBetweenRetries) async {
Duration pauseBetweenRetries, { Predicate predicate }) async {
assert(action != null);
assert(timeout != null);
assert(pauseBetweenRetries != null);
......@@ -28,20 +32,25 @@ Future<dynamic> retry(Action action, Duration timeout,
while(!success && sw.elapsed < timeout) {
try {
result = await action();
success = true;
if (predicate == null || predicate(result))
success = true;
lastError = null;
lastStackTrace = null;
} catch(error, stackTrace) {
lastError = error;
lastStackTrace = stackTrace;
if (sw.elapsed < timeout) {
await new Future<Null>.delayed(pauseBetweenRetries);
}
}
if (!success && sw.elapsed < timeout)
await new Future<Null>.delayed(pauseBetweenRetries);
}
if (success)
return result;
else
else if (lastError != null)
return new Future.error(lastError, lastStackTrace);
else
return new Future.error('Retry timed out');
}
/// A function that produces a [Stopwatch].
......
......@@ -122,9 +122,10 @@ main() {
test('finds by ValueKey', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], {
'kind': 'find_by_value_key',
'kind': 'find',
'searchSpecType': 'ByValueKey',
'keyValueString': 'foo',
'keyValueType': 'String',
'keyValueType': 'String'
});
return new Future.value({
'objectReferenceKey': '123',
......
......@@ -51,6 +51,26 @@ main() {
});
});
test('obeys predicates', () {
fakeAsync.run((_) {
int retryCount = 0;
expect(
// The predicate requires that the returned value is 2, so we expect
// that `retry` keeps trying until the counter reaches 2.
retry(
() async => retryCount++,
new Duration(milliseconds: 30),
new Duration(milliseconds: 10),
predicate: (value) => value == 2
),
completion(2)
);
fakeAsync.elapse(new Duration(milliseconds: 50));
});
});
test('times out returning last error', () async {
fakeAsync.run((_) {
bool timedOut = false;
......
......@@ -55,6 +55,20 @@ bool _addPackage(String directoryPath, List<String> dartFiles, Set<String> pubSp
}
}
Directory testDriverDirectory = new Directory(path.join(directoryPath, 'test_driver'));
if (testDriverDirectory.existsSync()) {
for (FileSystemEntity entry in testDriverDirectory.listSync()) {
if (entry is Directory) {
for (FileSystemEntity subentry in entry.listSync()) {
if (isDartTestFile(subentry))
dartFiles.add(subentry.path);
}
} else if (isDartTestFile(entry)) {
dartFiles.add(entry.path);
}
}
}
Directory benchmarkDirectory = new Directory(path.join(directoryPath, 'benchmark'));
if (benchmarkDirectory.existsSync()) {
for (FileSystemEntity entry in benchmarkDirectory.listSync()) {
......@@ -76,6 +90,18 @@ bool _addPackage(String directoryPath, List<String> dartFiles, Set<String> pubSp
return false;
}
/// Adds all packages in [subPath], assuming a flat directory structure, i.e.
/// each direct child of [subPath] is a plain Dart package.
void _addFlatPackageList(String subPath, List<String> dartFiles, Set<String> pubSpecDirectories) {
Directory subdirectory = new Directory(path.join(ArtifactStore.flutterRoot, subPath));
if (subdirectory.existsSync()) {
for (FileSystemEntity entry in subdirectory.listSync()) {
if (entry is Directory)
_addPackage(entry.path, dartFiles, pubSpecDirectories);
}
}
}
class FileChanged { }
class AnalyzeCommand extends FlutterCommand {
......@@ -146,23 +172,10 @@ class AnalyzeCommand extends FlutterCommand {
//dev/manual_tests/*/ as package
//dev/manual_tests/*/ as files
Directory subdirectory;
subdirectory = new Directory(path.join(ArtifactStore.flutterRoot, 'packages'));
if (subdirectory.existsSync()) {
for (FileSystemEntity entry in subdirectory.listSync()) {
if (entry is Directory)
_addPackage(entry.path, dartFiles, pubSpecDirectories);
}
}
_addFlatPackageList('packages', dartFiles, pubSpecDirectories);
_addFlatPackageList('examples', dartFiles, pubSpecDirectories);
subdirectory = new Directory(path.join(ArtifactStore.flutterRoot, 'examples'));
if (subdirectory.existsSync()) {
for (FileSystemEntity entry in subdirectory.listSync()) {
if (entry is Directory)
_addPackage(entry.path, dartFiles, pubSpecDirectories);
}
}
Directory subdirectory;
subdirectory = new Directory(path.join(ArtifactStore.flutterRoot, 'examples', 'layers'));
if (subdirectory.existsSync()) {
......@@ -198,7 +211,7 @@ class AnalyzeCommand extends FlutterCommand {
}
dartFiles = dartFiles.map((String directory) => path.normalize(path.absolute(directory))).toSet().toList();
dartFiles = dartFiles.map((String directory) => path.normalize(path.absolute(directory))).toSet().toList();
dartFiles.sort();
// prepare a Dart file that references all the above Dart files
......@@ -444,4 +457,3 @@ linter:
return 0;
}
}
......@@ -118,7 +118,7 @@ class DriveCommand extends RunCommandBase {
await appStopper(this);
} catch(error, stackTrace) {
// TODO(yjbanov): remove this guard when this bug is fixed: https://github.com/dart-lang/sdk/issues/25862
printStatus('Could not stop application: $error\n$stackTrace');
printTrace('Could not stop application: $error\n$stackTrace');
}
} else {
printStatus('Leaving the application running.');
......
import 'package:flutter/material.dart';
{{#withDriverTest?}}
import 'package:flutter_driver/driver_extension.dart';
{{/withDriverTest?}}
void main() {
{{#withDriverTest?}}
// Starts the app with Flutter Driver extension enabled to allow Flutter Driver
// to test the app.
enableFlutterDriverExtension();
{{/withDriverTest?}}
runApp(
new MaterialApp(
title: 'Flutter Demo',
......
// Starts the app with Flutter Driver extension enabled to allow Flutter Driver
// to test the app.
import 'package:{{projectName}}/main.dart' as app;
import 'package:flutter_driver/driver_extension.dart';
main() {
enableFlutterDriverExtension();
app.main();
}
......@@ -22,28 +22,20 @@ main() {
});
tearDownAll(() async {
if (driver != null) driver.close();
});
test('find the floating action button by value key', () async {
ObjectRef elem = await driver.findByValueKey('fab');
expect(elem, isNotNull);
expect(elem.objectReferenceKey, isNotNull);
if (driver != null)
driver.close();
});
test('tap on the floating action button; verify counter', () async {
// Find floating action button (fab) to tap on
ObjectRef fab = await driver.findByValueKey('fab');
ObjectRef fab = await driver.findByTooltipMessage('Increment');
expect(fab, isNotNull);
// Tap on the fab
await driver.tap(fab);
// Wait for text to change to the desired value
await driver.waitFor(() async {
ObjectRef counter = await driver.findByValueKey('counter');
return await driver.getText(counter);
}, contains("Button tapped 1 times."));
expect(await driver.findByText('Button tapped 1 time.'), isNotNull);
});
});
}
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