Reland text state (#47464)
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 372fb77..c56d747 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart
@@ -2961,7 +2961,7 @@ ), ), ); - expect(tester.testTextInput.editingState['text'], isEmpty); + expect(tester.testTextInput.editingState, isNull); // Initial state with null controller. await tester.tap(find.byType(TextField));
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index bfcaa00..e0f74ed 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -3928,10 +3928,6 @@ }); testWidgets('input imm channel calls are ordered correctly', (WidgetTester tester) async { - final List<MethodCall> log = <MethodCall>[]; - SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - }); const String testText = 'flutter is the best!'; final TextEditingController controller = TextEditingController(text: testText); final EditableText et = EditableText( @@ -3956,15 +3952,108 @@ )); await tester.showKeyboard(find.byType(EditableText)); - expect(log.length, 7); // TextInput.show should be before TextInput.setEditingState final List<String> logOrder = <String>['TextInput.setClient', 'TextInput.show', 'TextInput.setEditableSizeAndTransform', 'TextInput.setStyle', 'TextInput.setEditingState', 'TextInput.setEditingState', 'TextInput.show']; + expect(tester.testTextInput.log.length, 7); int index = 0; - for (MethodCall m in log) { + for (MethodCall m in tester.testTextInput.log) { expect(m.method, logOrder[index]); index++; } }); + + testWidgets('setEditingState is not called when text changes', (WidgetTester tester) async { + // We shouldn't get a message here because this change is owned by the platform side. + const String testText = 'flutter is the best!'; + final TextEditingController controller = TextEditingController(text: testText); + final EditableText et = EditableText( + showSelectionHandles: true, + maxLines: 2, + controller: controller, + focusNode: FocusNode(), + cursorColor: Colors.red, + backgroundCursorColor: Colors.blue, + style: Typography(platform: TargetPlatform.android).black.subhead.copyWith(fontFamily: 'Roboto'), + keyboardType: TextInputType.text, + ); + + await tester.pumpWidget(MaterialApp( + home: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 100, + child: et, + ), + ), + )); + + await tester.enterText(find.byType(EditableText), '...'); + + final List<String> logOrder = <String>[ + 'TextInput.setClient', + 'TextInput.show', + 'TextInput.setEditableSizeAndTransform', + 'TextInput.setStyle', + 'TextInput.setEditingState', + 'TextInput.setEditingState', + 'TextInput.show', + ]; + expect(tester.testTextInput.log.length, logOrder.length); + int index = 0; + for (MethodCall m in tester.testTextInput.log) { + expect(m.method, logOrder[index]); + index++; + } + expect(tester.testTextInput.editingState['text'], 'flutter is the best!'); + }); + + testWidgets('setEditingState is called when text changes on controller', (WidgetTester tester) async { + // We should get a message here because this change is owned by the framework side. + const String testText = 'flutter is the best!'; + final TextEditingController controller = TextEditingController(text: testText); + final EditableText et = EditableText( + showSelectionHandles: true, + maxLines: 2, + controller: controller, + focusNode: FocusNode(), + cursorColor: Colors.red, + backgroundCursorColor: Colors.blue, + style: Typography(platform: TargetPlatform.android).black.subhead.copyWith(fontFamily: 'Roboto'), + keyboardType: TextInputType.text, + ); + + await tester.pumpWidget(MaterialApp( + home: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 100, + child: et, + ), + ), + )); + + await tester.showKeyboard(find.byType(EditableText)); + controller.text += '...'; + await tester.idle(); + + final List<String> logOrder = <String>[ + 'TextInput.setClient', + 'TextInput.show', + 'TextInput.setEditableSizeAndTransform', + 'TextInput.setStyle', + 'TextInput.setEditingState', + 'TextInput.setEditingState', + 'TextInput.show', + 'TextInput.setEditingState', + ]; + expect(tester.testTextInput.log.length, logOrder.length); + int index = 0; + for (MethodCall m in tester.testTextInput.log) { + expect(m.method, logOrder[index]); + index++; + } + expect(tester.testTextInput.editingState['text'], 'flutter is the best!...'); + }); } class MockTextSelectionControls extends Mock implements TextSelectionControls {
diff --git a/packages/flutter_test/lib/src/test_text_input.dart b/packages/flutter_test/lib/src/test_text_input.dart index 665a674..53725bf 100644 --- a/packages/flutter_test/lib/src/test_text_input.dart +++ b/packages/flutter_test/lib/src/test_text_input.dart
@@ -39,6 +39,18 @@ /// The messenger which sends the bytes for this channel, not null. BinaryMessenger get _binaryMessenger => ServicesBinding.instance.defaultBinaryMessenger; + /// Resets any internal state of this object and calls [register]. + /// + /// This method is invoked by the testing framework between tests. It should + /// not ordinarily be called by tests directly. + void resetAndRegister() { + log.clear(); + editingState = null; + setClientArgs = null; + _client = 0; + _isVisible = false; + register(); + } /// Installs this object as a mock handler for [SystemChannels.textInput]. void register() { SystemChannels.textInput.setMockMethodCallHandler(_handleTextInputCall); @@ -64,6 +76,7 @@ /// Whether this [TestTextInput] is registered with [SystemChannels.textInput]. /// /// Use [register] and [unregister] methods to control this value. + // TODO(dnfield): This is unreliable. https://github.com/flutter/flutter/issues/47180 bool get isRegistered => _isRegistered; bool _isRegistered = false;
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index 9e9cd39..e54ae86 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -121,6 +121,7 @@ return binding.runTest( () async { debugResetSemanticsIdCounter(); + tester.resetTestTextInput(); await callback(tester); semanticsHandle?.dispose(); }, @@ -692,6 +693,17 @@ /// like [TextField] or [TextFormField], call [enterText]. TestTextInput get testTextInput => binding.testTextInput; + /// Ensures that [testTextInput] is registered and [TestTextInput.log] is + /// reset. + /// + /// This is called by the testing framework before test runs, so that if a + /// previous test has set its own handler on [SystemChannels.textInput], the + /// [testTextInput] regains control and the log is fresh for the new test. + /// It should not typically need to be called by tests. + void resetTestTextInput() { + testTextInput.resetAndRegister(); + } + /// Give the text input widget specified by [finder] the focus, as if the /// onscreen keyboard had appeared. ///