Added Driver API that waits until frame sync. (#36334)
diff --git a/packages/flutter_driver/lib/src/common/find.dart b/packages/flutter_driver/lib/src/common/find.dart
index 4eb6576..6bbb58b 100644
--- a/packages/flutter_driver/lib/src/common/find.dart
+++ b/packages/flutter_driver/lib/src/common/find.dart
@@ -119,6 +119,19 @@
String get kind => 'waitUntilNoTransientCallbacks';
}
+/// A Flutter Driver command that waits until the frame is synced.
+class WaitUntilFrameSync extends Command {
+ /// Creates a command that waits until there's no pending frame scheduled.
+ const WaitUntilFrameSync({ Duration timeout }) : super(timeout: timeout);
+
+ /// Deserializes this command from the value generated by [serialize].
+ WaitUntilFrameSync.deserialize(Map<String, String> json)
+ : super.deserialize(json);
+
+ @override
+ String get kind => 'waitUntilFrameSync';
+}
+
/// Base class for Flutter Driver finders, objects that describe how the driver
/// should search for elements.
abstract class SerializableFinder {
diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart
index d3558d7..bf9eeea 100644
--- a/packages/flutter_driver/lib/src/extension/extension.dart
+++ b/packages/flutter_driver/lib/src/extension/extension.dart
@@ -113,6 +113,7 @@
'waitFor': _waitFor,
'waitForAbsent': _waitForAbsent,
'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks,
+ 'waitUntilFrameSync': _waitUntilFrameSync,
'get_semantics_id': _getSemanticsId,
'get_offset': _getOffset,
'get_diagnostics_tree': _getDiagnosticsTree,
@@ -133,6 +134,7 @@
'waitFor': (Map<String, String> params) => WaitFor.deserialize(params),
'waitForAbsent': (Map<String, String> params) => WaitForAbsent.deserialize(params),
'waitUntilNoTransientCallbacks': (Map<String, String> params) => WaitUntilNoTransientCallbacks.deserialize(params),
+ 'waitUntilFrameSync': (Map<String, String> params) => WaitUntilFrameSync.deserialize(params),
'get_semantics_id': (Map<String, String> params) => GetSemanticsId.deserialize(params),
'get_offset': (Map<String, String> params) => GetOffset.deserialize(params),
'get_diagnostics_tree': (Map<String, String> params) => GetDiagnosticsTree.deserialize(params),
@@ -369,6 +371,31 @@
return null;
}
+ /// Returns a future that waits until frame is synced.
+ ///
+ /// Specifically, it checks:
+ /// * Whether the count of transient callbacks is zero.
+ /// * Whether there's no pending request for scheduling a new frame.
+ ///
+ /// We consider the frame is synced when both conditions are met.
+ ///
+ /// This method relies on a Flutter Driver mechanism called "frame sync",
+ /// which waits for transient animations to finish. Persistent animations will
+ /// cause this to wait forever.
+ ///
+ /// If a test needs to interact with the app while animations are running, it
+ /// should avoid this method and instead disable the frame sync using
+ /// `set_frame_sync` method. See [FlutterDriver.runUnsynchronized] for more
+ /// details on how to do this. Note, disabling frame sync will require the
+ /// test author to use some other method to avoid flakiness.
+ Future<Result> _waitUntilFrameSync(Command command) async {
+ await _waitUntilFrame(() {
+ return SchedulerBinding.instance.transientCallbackCount == 0
+ && !SchedulerBinding.instance.hasScheduledFrame;
+ });
+ return null;
+ }
+
Future<GetSemanticsIdResult> _getSemanticsId(Command command) async {
final GetSemanticsId semanticsCommand = command;
final Finder target = await _waitForElement(_createFinder(semanticsCommand.finder));
diff --git a/packages/flutter_driver/test/src/extension_test.dart b/packages/flutter_driver/test/src/extension_test.dart
index ad926d0..70d4b12 100644
--- a/packages/flutter_driver/test/src/extension_test.dart
+++ b/packages/flutter_driver/test/src/extension_test.dart
@@ -333,4 +333,81 @@
children = result['children'];
expect(children.single['children'], isEmpty);
});
+
+ group('waitUntilFrameSync', () {
+ FlutterDriverExtension extension;
+ Map<String, dynamic> result;
+
+ setUp(() {
+ extension = FlutterDriverExtension((String arg) async => '', true);
+ result = null;
+ });
+
+ testWidgets('returns immediately when frame is synced', (
+ WidgetTester tester) async {
+ extension.call(const WaitUntilFrameSync().serialize())
+ .then<void>(expectAsync1((Map<String, dynamic> r) {
+ result = r;
+ }));
+
+ await tester.idle();
+ expect(
+ result,
+ <String, dynamic>{
+ 'isError': false,
+ 'response': null,
+ },
+ );
+ });
+
+ testWidgets(
+ 'waits until no transient callbacks', (WidgetTester tester) async {
+ SchedulerBinding.instance.scheduleFrameCallback((_) {
+ // Intentionally blank. We only care about existence of a callback.
+ });
+
+ extension.call(const WaitUntilFrameSync().serialize())
+ .then<void>(expectAsync1((Map<String, dynamic> r) {
+ result = r;
+ }));
+
+ // Nothing should happen until the next frame.
+ await tester.idle();
+ expect(result, isNull);
+
+ // NOW we should receive the result.
+ await tester.pump();
+ expect(
+ result,
+ <String, dynamic>{
+ 'isError': false,
+ 'response': null,
+ },
+ );
+ });
+
+ testWidgets(
+ 'waits until no pending scheduled frame', (WidgetTester tester) async {
+ SchedulerBinding.instance.scheduleFrame();
+
+ extension.call(const WaitUntilFrameSync().serialize())
+ .then<void>(expectAsync1((Map<String, dynamic> r) {
+ result = r;
+ }));
+
+ // Nothing should happen until the next frame.
+ await tester.idle();
+ expect(result, isNull);
+
+ // NOW we should receive the result.
+ await tester.pump();
+ expect(
+ result,
+ <String, dynamic>{
+ 'isError': false,
+ 'response': null,
+ },
+ );
+ });
+ });
}