[flutter_driver] Add waitForTappable to flutter_driver (#79581)

diff --git a/packages/flutter_driver/lib/src/common/deserialization_factory.dart b/packages/flutter_driver/lib/src/common/deserialization_factory.dart
index 215b0ed..97cd532 100644
--- a/packages/flutter_driver/lib/src/common/deserialization_factory.dart
+++ b/packages/flutter_driver/lib/src/common/deserialization_factory.dart
@@ -56,6 +56,7 @@
       case 'tap': return Tap.deserialize(params, finderFactory);
       case 'waitFor': return WaitFor.deserialize(params, finderFactory);
       case 'waitForAbsent': return WaitForAbsent.deserialize(params, finderFactory);
+      case 'waitForTappable': return WaitForTappable.deserialize(params, finderFactory);
       case 'waitForCondition': return WaitForCondition.deserialize(params);
       case 'waitUntilNoTransientCallbacks': return WaitForCondition.deserialize(params);
       case 'waitUntilNoPendingFrame': return WaitForCondition.deserialize(params);
diff --git a/packages/flutter_driver/lib/src/common/find.dart b/packages/flutter_driver/lib/src/common/find.dart
index b3cd5f6..cca7c0b 100644
--- a/packages/flutter_driver/lib/src/common/find.dart
+++ b/packages/flutter_driver/lib/src/common/find.dart
@@ -80,6 +80,24 @@
   String get kind => 'waitForAbsent';
 }
 
+/// A Flutter Driver command that waits until [finder] can be tapped.
+class WaitForTappable extends CommandWithTarget {
+  /// Creates a command that waits for the widget identified by [finder] to
+  /// be tappable within the [timeout] amiount of time.
+  ///
+  /// If [timeout] is not specified, the command defuts to no timeout.
+  WaitForTappable(SerializableFinder finder, {Duration? timeout})
+      : super(finder, timeout: timeout);
+
+  /// Deserialized this command from the value generated by [serialize].
+  WaitForTappable.deserialize(
+      Map<String, String> json, DeserializeFinderFactory finderFactory)
+      : super.deserialize(json, finderFactory);
+
+  @override
+  String get kind => 'waitForTappable';
+}
+
 /// 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/common/handler_factory.dart b/packages/flutter_driver/lib/src/common/handler_factory.dart
index f0ba022..41ccd3a 100644
--- a/packages/flutter_driver/lib/src/common/handler_factory.dart
+++ b/packages/flutter_driver/lib/src/common/handler_factory.dart
@@ -169,6 +169,7 @@
       case 'tap': return _tap(command, prober, finderFactory);
       case 'waitFor': return _waitFor(command, finderFactory);
       case 'waitForAbsent': return _waitForAbsent(command, finderFactory);
+      case 'waitForTappable': return _waitForTappable(command, finderFactory);
       case 'waitForCondition': return _waitForCondition(command);
       case 'waitUntilNoTransientCallbacks': return _waitUntilNoTransientCallbacks(command);
       case 'waitUntilNoPendingFrame': return _waitUntilNoPendingFrame(command);
@@ -236,6 +237,14 @@
     return Result.empty;
   }
 
+  Future<Result> _waitForTappable(Command command, CreateFinderFactory finderFactory) async {
+    final WaitForTappable waitForTappableCommand = command as WaitForTappable;
+    await waitForElement(
+      finderFactory.createFinder(waitForTappableCommand.finder).hitTestable(),
+    );
+    return Result.empty;
+  }
+
   Future<Result> _waitForCondition(Command command) async {
     assert(command != null);
     final WaitForCondition waitForConditionCommand = command as WaitForCondition;
diff --git a/packages/flutter_driver/lib/src/driver/driver.dart b/packages/flutter_driver/lib/src/driver/driver.dart
index 9b20532..56b7223 100644
--- a/packages/flutter_driver/lib/src/driver/driver.dart
+++ b/packages/flutter_driver/lib/src/driver/driver.dart
@@ -220,6 +220,11 @@
     await sendCommand(WaitForAbsent(finder, timeout: timeout));
   }
 
+  /// Waits until [finder] is tappable.
+  Future<void> waitForTappable(SerializableFinder finder, { Duration? timeout }) async {
+    await sendCommand(WaitForTappable(finder, timeout: timeout));
+  }
+
   /// Waits until the given [waitCondition] is satisfied.
   Future<void> waitForCondition(SerializableWaitCondition waitCondition, {Duration? timeout}) async {
     await sendCommand(WaitForCondition(waitCondition, timeout: timeout));
diff --git a/packages/flutter_driver/test/src/real_tests/extension_test.dart b/packages/flutter_driver/test/src/real_tests/extension_test.dart
index 638bacc..6857a21 100644
--- a/packages/flutter_driver/test/src/real_tests/extension_test.dart
+++ b/packages/flutter_driver/test/src/real_tests/extension_test.dart
@@ -1130,6 +1130,41 @@
     });
   });
 
+  group('waitForTappable', () {
+    late FlutterDriverExtension driverExtension;
+
+    Future<Map<String, dynamic>> waitForTappable() async {
+      final SerializableFinder finder = ByValueKey('widgetOne');
+      final Map<String, String> arguments = WaitForTappable(finder).serialize();
+      final Map<String, dynamic> result = await driverExtension.call(arguments);
+      return result;
+    }
+
+    final Widget testWidget = MaterialApp(
+      home: Material(
+        child: Column(children: const<Widget> [
+          Text('Hello ', key: Key('widgetOne')),
+          SizedBox(
+            height: 0,
+            width: 0,
+            child: Text('World!', key: Key('widgetTwo')),
+            )
+          ],
+        ),
+      ),
+    );
+
+    testWidgets('returns true when widget is tappable', (
+        WidgetTester tester) async {
+      driverExtension = FlutterDriverExtension((String? arg) async => '', true, false);
+
+      await tester.pumpWidget(testWidget);
+
+      final Map<String, dynamic> waitForTappableResult = await waitForTappable();
+      expect(waitForTappableResult['isError'], isFalse);
+    });
+  });
+
   group('waitUntilFrameSync', () {
     late FlutterDriverExtension driverExtension;
     Map<String, dynamic>? result;