Commit e7657b94 authored by Yegor's avatar Yegor

[driver] "waitFor" command in place of broken "exists" (#3373)

* [driver] "waitFor" command in place of broken "exits"

* [driver] wait using frame callback
parent a2ce9483
......@@ -5,7 +5,6 @@
import 'dart:async';
import 'dart:io';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:matcher/matcher.dart';
import 'package:vm_service_client/vm_service_client.dart';
import 'package:web_socket_channel/io.dart';
......@@ -13,9 +12,7 @@ import 'error.dart';
import 'find.dart';
import 'gesture.dart';
import 'health.dart';
import 'matcher_util.dart';
import 'message.dart';
import 'retry.dart';
import 'timeline.dart';
final Logger _log = new Logger('FlutterDriver');
......@@ -43,7 +40,6 @@ class FlutterDriver {
static const String _kSetVMTimelineFlagsMethod = '_setVMTimelineFlags';
static const String _kGetVMTimelineMethod = '_getVMTimeline';
static const Duration _kDefaultTimeout = const Duration(seconds: 5);
static const Duration _kDefaultPauseBetweenRetries = const Duration(milliseconds: 160);
/// Connects to a Flutter application.
///
......@@ -188,12 +184,14 @@ class FlutterDriver {
/// 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);
await _sendCommand(new Tap(finder));
return 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);
/// Waits until [finder] locates the target.
Future<Null> waitFor(SerializableFinder finder, {Duration timeout: _kDefaultTimeout}) async {
await _sendCommand(new WaitFor(finder, timeout: timeout));
return null;
}
/// Tell the driver to perform a scrolling action.
......@@ -259,24 +257,6 @@ class FlutterDriver {
return stopTracingAndDownloadTimeline();
}
/// Calls the [evaluator] repeatedly until the result of the evaluation
/// satisfies the [matcher].
///
/// Returns the result of the evaluation.
Future<String> waitFor(EvaluatorFunction evaluator, Matcher matcher, {
Duration timeout: _kDefaultTimeout,
Duration pauseBetweenRetries: _kDefaultPauseBetweenRetries
}) async {
return retry(() async {
dynamic value = await evaluator();
MatchResult matchResult = match(value, matcher);
if (!matchResult.hasMatched) {
return new Future<Null>.error(matchResult.mismatchDescription);
}
return value;
}, timeout, pauseBetweenRetries);
}
/// Closes the underlying connection to the VM service.
///
/// Returns a [Future] that fires once the connection has been closed.
......
......@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/src/instrumentation.dart';
......@@ -16,11 +17,9 @@ import 'find.dart';
import 'gesture.dart';
import 'health.dart';
import 'message.dart';
import 'retry.dart';
const String _extensionMethod = 'ext.flutter_driver';
const Duration _kDefaultTimeout = const Duration(seconds: 5);
const Duration _kDefaultPauseBetweenRetries = const Duration(milliseconds: 160);
bool _flutterDriverExtensionEnabled = false;
......@@ -54,32 +53,33 @@ class FlutterDriverExtension {
static final Logger _log = new Logger('FlutterDriverExtension');
FlutterDriverExtension() {
_commandHandlers = <String, CommandHandlerCallback>{
_commandHandlers.addAll(<String, CommandHandlerCallback>{
'get_health': getHealth,
'tap': tap,
'get_text': getText,
'scroll': scroll,
};
'waitFor': waitFor,
});
_commandDeserializers = <String, CommandDeserializerCallback>{
_commandDeserializers.addAll(<String, CommandDeserializerCallback>{
'get_health': GetHealth.deserialize,
'tap': Tap.deserialize,
'get_text': GetText.deserialize,
'scroll': Scroll.deserialize,
};
'waitFor': WaitFor.deserialize,
});
_finders = <String, FinderCallback>{
_finders.addAll(<String, FinderCallback>{
'ByValueKey': _findByValueKey,
'ByTooltipMessage': _findByTooltipMessage,
'ByText': _findByText,
};
});
}
final Instrumentation prober = new Instrumentation();
Map<String, CommandHandlerCallback> _commandHandlers;
Map<String, CommandDeserializerCallback> _commandDeserializers;
Map<String, FinderCallback> _finders;
final Map<String, CommandHandlerCallback> _commandHandlers = <String, CommandHandlerCallback>{};
final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{};
final Map<String, FinderCallback> _finders = <String, FinderCallback>{};
Future<ServiceExtensionResponse> call(Map<String, String> params) async {
try {
......@@ -110,16 +110,42 @@ class FlutterDriverExtension {
}
}
Stream<Duration> _onFrameReadyStream;
Stream<Duration> get _onFrameReady {
if (_onFrameReadyStream == null) {
// Lazy-initialize the frame callback because the renderer is not yet
// available at the time the extension is registered.
StreamController<Duration> frameReadyController = new StreamController<Duration>.broadcast(sync: true);
Scheduler.instance.addPersistentFrameCallback((Duration timestamp) {
frameReadyController.add(timestamp);
});
_onFrameReadyStream = frameReadyController.stream;
}
return _onFrameReadyStream;
}
Future<Health> getHealth(GetHealth command) async => new Health(HealthStatus.ok);
/// Runs object [finder] repeatedly until it finds an [Element].
/// Runs [locator] repeatedly until it finds an [Element] or times out.
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;
Completer<Element> completer = new Completer<Element>();
StreamSubscription<Duration> subscription;
Timer timeout = new Timer(_kDefaultTimeout, () {
subscription.cancel();
completer.completeError('Timed out waiting for ${descriptionGetter()}');
});
subscription = _onFrameReady.listen((Duration duration) {
Element element = locator();
if (element != null) {
subscription.cancel();
timeout.cancel();
completer.complete(element);
}
});
return completer.future;
}
Future<Element> _findByValueKey(ByValueKey byKey) async {
......@@ -170,6 +196,13 @@ class FlutterDriverExtension {
return new TapResult();
}
Future<WaitForResult> waitFor(WaitFor command) async {
if (await _runFinder(command.finder) != null)
return new WaitForResult();
else
return null;
}
Future<ScrollResult> scroll(Scroll command) async {
Element target = await _runFinder(command.finder);
final int totalMoves = command.duration.inMicroseconds * command.frequency ~/ Duration.MICROSECONDS_PER_SECOND;
......
......@@ -37,35 +37,44 @@ abstract class CommandWithTarget extends Command {
Map<String, String> serialize() => finder.serialize();
}
/// Checks if the widget identified by the given finder exists.
class Exists extends CommandWithTarget {
/// Waits until [finder] can locate the target.
class WaitFor extends CommandWithTarget {
@override
final String kind = 'exists';
final String kind = 'waitFor';
Exists(SerializableFinder finder) : super(finder);
WaitFor(SerializableFinder finder, {this.timeout})
: super(finder);
static Exists deserialize(Map<String, String> json) {
return new Exists(SerializableFinder.deserialize(json));
final Duration timeout;
static WaitFor deserialize(Map<String, String> json) {
Duration timeout = json['timeout'] != null
? new Duration(milliseconds: int.parse(json['timeout']))
: null;
return new WaitFor(SerializableFinder.deserialize(json), timeout: timeout);
}
@override
Map<String, String> serialize() => super.serialize();
}
Map<String, String> serialize() {
Map<String, String> json = super.serialize();
class ExistsResult extends Result {
ExistsResult(this.exists);
if (timeout != null) {
json['timeout'] = '${timeout.inMilliseconds}';
}
static ExistsResult fromJson(Map<String, dynamic> json) {
return new ExistsResult(json['exists']);
return json;
}
}
/// Whether the widget was found on the UI or not.
final bool exists;
class WaitForResult extends Result {
WaitForResult();
static WaitForResult fromJson(Map<String, dynamic> json) {
return new WaitForResult();
}
@override
Map<String, dynamic> toJson() => {
'exists': exists,
};
Map<String, dynamic> toJson() => <String, dynamic>{};
}
/// Describes how to the driver should search for elements.
......
......@@ -10,7 +10,6 @@ import 'package:flutter_driver/src/health.dart';
import 'package:flutter_driver/src/timeline.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
import 'package:mockito/mockito.dart';
import 'package:quiver/testing/async.dart';
import 'package:test/test.dart';
import 'package:vm_service_client/vm_service_client.dart';
......@@ -180,54 +179,21 @@ void main() {
});
group('waitFor', () {
test('waits for a condition', () {
expect(
driver.waitFor(() {
return new Future<int>.delayed(
new Duration(milliseconds: 50),
() => 123
);
}, equals(123)),
completion(123)
);
});
test('retries a correct number of times', () {
new FakeAsync().run((FakeAsync fakeAsync) {
int retryCount = 0;
expect(
driver.waitFor(
() {
retryCount++;
return retryCount;
},
equals(2),
timeout: new Duration(milliseconds: 30),
pauseBetweenRetries: new Duration(milliseconds: 10)
),
completion(2)
);
fakeAsync.elapse(new Duration(milliseconds: 50));
// Check that we didn't retry more times than necessary
expect(retryCount, 2);
});
test('requires a target reference', () async {
expect(driver.waitFor(null), throwsA(new isInstanceOf<DriverError>()));
});
test('times out', () async {
bool timedOut = false;
await driver.waitFor(
() => 1,
equals(2),
timeout: new Duration(milliseconds: 10),
pauseBetweenRetries: new Duration(milliseconds: 2)
).catchError((dynamic err, dynamic stack) {
timedOut = true;
test('sends the waitFor command', () async {
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
expect(i.positionalArguments[1], <String, dynamic>{
'command': 'waitFor',
'finderType': 'ByTooltipMessage',
'text': 'foo',
'timeout': '1000',
});
return new Future<Map<String, dynamic>>.value({});
});
expect(timedOut, isTrue);
await driver.waitFor(find.byTooltip('foo'), timeout: new Duration(seconds: 1));
});
});
......
......@@ -22,15 +22,17 @@ void main() {
});
test('tap on the floating action button; verify counter', () async {
// Find floating action button (fab) to tap on
// Finds the floating action button (fab) to tap on
SerializableFinder fab = find.byTooltip('Increment');
expect(await driver.exists(fab), isTrue);
// Wait for the floating action button to appear
await driver.waitFor(fab);
// Tap on the fab
await driver.tap(fab);
// Wait for text to change to the desired value
expect(await driver.exists(find.text('Button tapped 1 time.')), isTrue);
await driver.waitFor(find.text('Button tapped 1 time.'));
});
});
}
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