Clear the keyboard state in the test framework when keyboard is closed. (#18615)

diff --git a/packages/flutter/test/material/text_form_field_test.dart b/packages/flutter/test/material/text_form_field_test.dart
index 872455c..263c560 100644
--- a/packages/flutter/test/material/text_form_field_test.dart
+++ b/packages/flutter/test/material/text_form_field_test.dart
@@ -49,7 +49,7 @@
     await tester.pump();
     expect(_called, true);
   });
-  
+
   testWidgets('autovalidate is passed to super', (WidgetTester tester) async {
     int _validateCalled = 0;
 
diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart
index 4756b5c..526e1e1 100644
--- a/packages/flutter/test/widgets/form_test.dart
+++ b/packages/flutter/test/widgets/form_test.dart
@@ -99,8 +99,9 @@
 
     Future<Null> checkErrorText(String testValue) async {
       formKey.currentState.reset();
-      await tester.enterText(find.byType(TextFormField), testValue);
       await tester.pumpWidget(builder(false));
+      await tester.enterText(find.byType(TextFormField), testValue);
+      await tester.pump();
 
       // We have to manually validate if we're not autovalidating.
       expect(find.text(errorText(testValue)), findsNothing);
@@ -110,8 +111,9 @@
 
       // Try again with autovalidation. Should validate immediately.
       formKey.currentState.reset();
-      await tester.enterText(find.byType(TextFormField), testValue);
       await tester.pumpWidget(builder(true));
+      await tester.enterText(find.byType(TextFormField), testValue);
+      await tester.pump();
 
       expect(find.text(errorText(testValue)), findsOneWidget);
     }
diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart
index 403ba11..a149ceb 100644
--- a/packages/flutter_test/lib/src/binding.dart
+++ b/packages/flutter_test/lib/src/binding.dart
@@ -152,7 +152,7 @@
   void initInstances() {
     timeDilation = 1.0; // just in case the developer has artificially changed it for development
     HttpOverrides.global = new _MockHttpOverrides();
-    _testTextInput = new TestTextInput()..register();
+    _testTextInput = new TestTextInput(onCleared: _resetFocusedEditable)..register();
     super.initInstances();
   }
 
@@ -280,10 +280,20 @@
   /// The current client of the onscreen keyboard. Callers must pump
   /// an additional frame after setting this property to complete the
   /// the focus change.
+  ///
+  /// Instead of setting this directly, consider using
+  /// [WidgetTester.showKeyboard].
   EditableTextState get focusedEditable => _focusedEditable;
   EditableTextState _focusedEditable;
   set focusedEditable(EditableTextState value) {
-    _focusedEditable = value..requestKeyboard();
+    if (_focusedEditable != value) {
+      _focusedEditable = value;
+      value?.requestKeyboard();
+    }
+  }
+
+  void _resetFocusedEditable() {
+    _focusedEditable = null;
   }
 
   /// Returns the exception most recently caught by the Flutter framework.
diff --git a/packages/flutter_test/lib/src/test_text_input.dart b/packages/flutter_test/lib/src/test_text_input.dart
index 436a1c8..e8bebca 100644
--- a/packages/flutter_test/lib/src/test_text_input.dart
+++ b/packages/flutter_test/lib/src/test_text_input.dart
@@ -6,9 +6,12 @@
 import 'dart:typed_data';
 
 import 'package:flutter/services.dart';
+import 'package:flutter/foundation.dart';
 
 import 'widget_tester.dart';
 
+export 'package:flutter/services.dart' show TextEditingValue, TextInputAction;
+
 /// A testing stub for the system's onscreen keyboard.
 ///
 /// Typical app tests will not need to use this class directly.
@@ -19,6 +22,19 @@
 /// * [WidgetTester.showKeyboard], which uses this class to simulate showing the
 ///   popup keyboard and initializing its text.
 class TestTextInput {
+  /// Create a fake keyboard backend.
+  ///
+  /// The [onCleared] argument may be set to be notified of when the keyboard
+  /// is dismissed.
+  TestTextInput({ this.onCleared });
+
+  /// Called when the keyboard goes away.
+  ///
+  /// To use the methods on this API that send fake keyboard messages (such as
+  /// [updateEditingValue], [enterText], or [receiveAction]), the keyboard must
+  /// first be requested, e.g. using [WidgetTester.showKeyboard].
+  final VoidCallback onCleared;
+
   /// Installs this object as a mock handler for [SystemChannels.textInput].
   void register() {
     SystemChannels.textInput.setMockMethodCallHandler(_handleTextInputCall);
@@ -63,6 +79,8 @@
       case 'TextInput.clearClient':
         _client = 0;
         _isVisible = false;
+        if (onCleared != null)
+          onCleared();
         break;
       case 'TextInput.setEditingState':
         editingState = methodCall.arguments;
@@ -84,9 +102,8 @@
   void updateEditingValue(TextEditingValue value) {
     // Not using the `expect` function because in the case of a FlutterDriver
     // test this code does not run in a package:test test zone.
-    if (_client == 0) {
-      throw new TestFailure('_client must be non-zero');
-    }
+    if (_client == 0)
+      throw new TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.');
     BinaryMessages.handlePlatformMessage(
       SystemChannels.textInput.name,
       SystemChannels.textInput.codec.encodeMethodCall(
@@ -112,9 +129,8 @@
   void receiveAction(TextInputAction action) {
     // Not using the `expect` function because in the case of a FlutterDriver
     // test this code does not run in a package:test test zone.
-    if (_client == 0) {
-      throw new TestFailure('_client must be non-zero');
-    }
+    if (_client == 0)
+      throw new TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.');
     BinaryMessages.handlePlatformMessage(
       SystemChannels.textInput.name,
       SystemChannels.textInput.codec.encodeMethodCall(
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index f3fb46b..ae51e57 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -558,6 +558,8 @@
   /// Give the text input widget specified by [finder] the focus, as if the
   /// onscreen keyboard had appeared.
   ///
+  /// Implies a call to [pump].
+  ///
   /// The widget specified by [finder] must be an [EditableText] or have
   /// an [EditableText] descendant. For example `find.byType(TextField)`
   /// or `find.byType(TextFormField)`, or `find.byType(EditableText)`.
@@ -566,15 +568,15 @@
   /// or [TextFormField] only need to call [enterText].
   Future<Null> showKeyboard(Finder finder) async {
     return TestAsyncUtils.guard(() async {
-      final EditableTextState editable = state(find.descendant(
-        of: finder,
-        matching: find.byType(EditableText),
-        matchRoot: true,
-      ));
-      if (editable != binding.focusedEditable) {
-        binding.focusedEditable = editable;
-        await pump();
-      }
+      final EditableTextState editable = state(
+        find.descendant(
+          of: finder,
+          matching: find.byType(EditableText),
+          matchRoot: true,
+        ),
+      );
+      binding.focusedEditable = editable;
+      await pump();
     });
   }
 
diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart
index d261b84..441d59d 100644
--- a/packages/flutter_test/test/widget_tester_test.dart
+++ b/packages/flutter_test/test/widget_tester_test.dart
@@ -506,6 +506,27 @@
       });
     });
   });
+
+  testWidgets('showKeyboard can be called twice', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      new MaterialApp(
+        home: new Material(
+          child: new Center(
+            child: new TextFormField(),
+          ),
+        ),
+      ),
+    );
+    await tester.showKeyboard(find.byType(TextField));
+    tester.testTextInput.receiveAction(TextInputAction.done);
+    await tester.pump();
+    await tester.showKeyboard(find.byType(TextField));
+    tester.testTextInput.receiveAction(TextInputAction.done);
+    await tester.pump();
+    await tester.showKeyboard(find.byType(TextField));
+    await tester.showKeyboard(find.byType(TextField));
+    await tester.pump();
+  });
 }
 
 class FakeMatcher extends AsyncMatcher {