From 4ad8271bf50b97b3cbdf108f91cbf070033466c3 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Thu, 19 Dec 2019 13:43:02 -0800 Subject: [PATCH] Reland text state (#47464) --- .../test/material/text_field_test.dart | 2 +- .../test/widgets/editable_text_test.dart | 101 ++++++++++++++++-- .../flutter_test/lib/src/test_text_input.dart | 13 +++ .../flutter_test/lib/src/widget_tester.dart | 12 +++ 4 files changed, 121 insertions(+), 7 deletions(-) diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 372fb77f7..c56d74705 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -2961,7 +2961,7 @@ void main() { ), ), ); - 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 bfcaa00fd..e0f74ed5e 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -3928,10 +3928,6 @@ void main() { }); testWidgets('input imm channel calls are ordered correctly', (WidgetTester tester) async { - final List log = []; - 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 @@ void main() { )); await tester.showKeyboard(find.byType(EditableText)); - expect(log.length, 7); // TextInput.show should be before TextInput.setEditingState final List logOrder = ['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 logOrder = [ + '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 logOrder = [ + '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 665a674b7..53725bf9c 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 @@ class TestTextInput { /// 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 @@ class TestTextInput { /// 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 9e9cd397e..e54ae860c 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -121,6 +121,7 @@ void testWidgets( return binding.runTest( () async { debugResetSemanticsIdCounter(); + tester.resetTestTextInput(); await callback(tester); semanticsHandle?.dispose(); }, @@ -692,6 +693,17 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker /// 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. ///