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,
+        },
+      );
+    });
+  });
 }