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 @@ ...@@ -5,7 +5,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:json_rpc_2/json_rpc_2.dart' as rpc; 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:vm_service_client/vm_service_client.dart';
import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/io.dart';
...@@ -13,9 +12,7 @@ import 'error.dart'; ...@@ -13,9 +12,7 @@ import 'error.dart';
import 'find.dart'; import 'find.dart';
import 'gesture.dart'; import 'gesture.dart';
import 'health.dart'; import 'health.dart';
import 'matcher_util.dart';
import 'message.dart'; import 'message.dart';
import 'retry.dart';
import 'timeline.dart'; import 'timeline.dart';
final Logger _log = new Logger('FlutterDriver'); final Logger _log = new Logger('FlutterDriver');
...@@ -43,7 +40,6 @@ class FlutterDriver { ...@@ -43,7 +40,6 @@ class FlutterDriver {
static const String _kSetVMTimelineFlagsMethod = '_setVMTimelineFlags'; static const String _kSetVMTimelineFlagsMethod = '_setVMTimelineFlags';
static const String _kGetVMTimelineMethod = '_getVMTimeline'; static const String _kGetVMTimelineMethod = '_getVMTimeline';
static const Duration _kDefaultTimeout = const Duration(seconds: 5); static const Duration _kDefaultTimeout = const Duration(seconds: 5);
static const Duration _kDefaultPauseBetweenRetries = const Duration(milliseconds: 160);
/// Connects to a Flutter application. /// Connects to a Flutter application.
/// ///
...@@ -188,12 +184,14 @@ class FlutterDriver { ...@@ -188,12 +184,14 @@ class FlutterDriver {
/// Taps at the center of the widget located by [finder]. /// Taps at the center of the widget located by [finder].
Future<Null> tap(SerializableFinder finder) async { 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. /// Waits until [finder] locates the target.
Future<bool> exists(SerializableFinder finder) async { Future<Null> waitFor(SerializableFinder finder, {Duration timeout: _kDefaultTimeout}) async {
return await _sendCommand(new Exists(finder)).then((Map<String, dynamic> _) => null); await _sendCommand(new WaitFor(finder, timeout: timeout));
return null;
} }
/// Tell the driver to perform a scrolling action. /// Tell the driver to perform a scrolling action.
...@@ -259,24 +257,6 @@ class FlutterDriver { ...@@ -259,24 +257,6 @@ class FlutterDriver {
return stopTracingAndDownloadTimeline(); 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. /// Closes the underlying connection to the VM service.
/// ///
/// Returns a [Future] that fires once the connection has been closed. /// Returns a [Future] that fires once the connection has been closed.
......
...@@ -6,6 +6,7 @@ import 'dart:async'; ...@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter_test/src/instrumentation.dart'; import 'package:flutter_test/src/instrumentation.dart';
...@@ -16,11 +17,9 @@ import 'find.dart'; ...@@ -16,11 +17,9 @@ import 'find.dart';
import 'gesture.dart'; import 'gesture.dart';
import 'health.dart'; import 'health.dart';
import 'message.dart'; import 'message.dart';
import 'retry.dart';
const String _extensionMethod = 'ext.flutter_driver'; const String _extensionMethod = 'ext.flutter_driver';
const Duration _kDefaultTimeout = const Duration(seconds: 5); const Duration _kDefaultTimeout = const Duration(seconds: 5);
const Duration _kDefaultPauseBetweenRetries = const Duration(milliseconds: 160);
bool _flutterDriverExtensionEnabled = false; bool _flutterDriverExtensionEnabled = false;
...@@ -54,32 +53,33 @@ class FlutterDriverExtension { ...@@ -54,32 +53,33 @@ class FlutterDriverExtension {
static final Logger _log = new Logger('FlutterDriverExtension'); static final Logger _log = new Logger('FlutterDriverExtension');
FlutterDriverExtension() { FlutterDriverExtension() {
_commandHandlers = <String, CommandHandlerCallback>{ _commandHandlers.addAll(<String, CommandHandlerCallback>{
'get_health': getHealth, 'get_health': getHealth,
'tap': tap, 'tap': tap,
'get_text': getText, 'get_text': getText,
'scroll': scroll, 'scroll': scroll,
}; 'waitFor': waitFor,
});
_commandDeserializers = <String, CommandDeserializerCallback>{ _commandDeserializers.addAll(<String, CommandDeserializerCallback>{
'get_health': GetHealth.deserialize, 'get_health': GetHealth.deserialize,
'tap': Tap.deserialize, 'tap': Tap.deserialize,
'get_text': GetText.deserialize, 'get_text': GetText.deserialize,
'scroll': Scroll.deserialize, 'scroll': Scroll.deserialize,
}; 'waitFor': WaitFor.deserialize,
});
_finders = <String, FinderCallback>{ _finders.addAll(<String, FinderCallback>{
'ByValueKey': _findByValueKey, 'ByValueKey': _findByValueKey,
'ByTooltipMessage': _findByTooltipMessage, 'ByTooltipMessage': _findByTooltipMessage,
'ByText': _findByText, 'ByText': _findByText,
}; });
} }
final Instrumentation prober = new Instrumentation(); final Instrumentation prober = new Instrumentation();
final Map<String, CommandHandlerCallback> _commandHandlers = <String, CommandHandlerCallback>{};
Map<String, CommandHandlerCallback> _commandHandlers; final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{};
Map<String, CommandDeserializerCallback> _commandDeserializers; final Map<String, FinderCallback> _finders = <String, FinderCallback>{};
Map<String, FinderCallback> _finders;
Future<ServiceExtensionResponse> call(Map<String, String> params) async { Future<ServiceExtensionResponse> call(Map<String, String> params) async {
try { try {
...@@ -110,16 +110,42 @@ class FlutterDriverExtension { ...@@ -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); 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()) { Future<Element> _waitForElement(String descriptionGetter(), Element locator()) {
return retry(locator, _kDefaultTimeout, _kDefaultPauseBetweenRetries, predicate: (dynamic object) { Completer<Element> completer = new Completer<Element>();
return object != null; StreamSubscription<Duration> subscription;
}).catchError((Object error, Object stackTrace) {
_log.warning('Timed out waiting for ${descriptionGetter()}'); Timer timeout = new Timer(_kDefaultTimeout, () {
return null; 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 { Future<Element> _findByValueKey(ByValueKey byKey) async {
...@@ -170,6 +196,13 @@ class FlutterDriverExtension { ...@@ -170,6 +196,13 @@ class FlutterDriverExtension {
return new TapResult(); 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 { Future<ScrollResult> scroll(Scroll command) async {
Element target = await _runFinder(command.finder); 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;
......
...@@ -37,35 +37,44 @@ abstract class CommandWithTarget extends Command { ...@@ -37,35 +37,44 @@ abstract class CommandWithTarget extends Command {
Map<String, String> serialize() => finder.serialize(); Map<String, String> serialize() => finder.serialize();
} }
/// Checks if the widget identified by the given finder exists. /// Waits until [finder] can locate the target.
class Exists extends CommandWithTarget { class WaitFor extends CommandWithTarget {
@override @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) { final Duration timeout;
return new Exists(SerializableFinder.deserialize(json));
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 @override
Map<String, String> serialize() => super.serialize(); Map<String, String> serialize() {
} Map<String, String> json = super.serialize();
class ExistsResult extends Result { if (timeout != null) {
ExistsResult(this.exists); json['timeout'] = '${timeout.inMilliseconds}';
}
static ExistsResult fromJson(Map<String, dynamic> json) { return json;
return new ExistsResult(json['exists']);
} }
}
/// Whether the widget was found on the UI or not. class WaitForResult extends Result {
final bool exists; WaitForResult();
static WaitForResult fromJson(Map<String, dynamic> json) {
return new WaitForResult();
}
@override @override
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => <String, dynamic>{};
'exists': exists,
};
} }
/// Describes how to the driver should search for elements. /// Describes how to the driver should search for elements.
......
...@@ -10,7 +10,6 @@ import 'package:flutter_driver/src/health.dart'; ...@@ -10,7 +10,6 @@ import 'package:flutter_driver/src/health.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';
import 'package:quiver/testing/async.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:vm_service_client/vm_service_client.dart'; import 'package:vm_service_client/vm_service_client.dart';
...@@ -180,54 +179,21 @@ void main() { ...@@ -180,54 +179,21 @@ void main() {
}); });
group('waitFor', () { group('waitFor', () {
test('waits for a condition', () { test('requires a target reference', () async {
expect( expect(driver.waitFor(null), throwsA(new isInstanceOf<DriverError>()));
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('times out', () async { test('sends the waitFor command', () async {
bool timedOut = false; when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
await driver.waitFor( expect(i.positionalArguments[1], <String, dynamic>{
() => 1, 'command': 'waitFor',
equals(2), 'finderType': 'ByTooltipMessage',
timeout: new Duration(milliseconds: 10), 'text': 'foo',
pauseBetweenRetries: new Duration(milliseconds: 2) 'timeout': '1000',
).catchError((dynamic err, dynamic stack) { });
timedOut = true; return new Future<Map<String, dynamic>>.value({});
}); });
await driver.waitFor(find.byTooltip('foo'), timeout: new Duration(seconds: 1));
expect(timedOut, isTrue);
}); });
}); });
......
...@@ -22,15 +22,17 @@ void main() { ...@@ -22,15 +22,17 @@ 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 // Finds the floating action button (fab) to tap on
SerializableFinder fab = find.byTooltip('Increment'); 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 // 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.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