Skip to content

Commit b070ed3

Browse files
authored
Fix a legacy TODO (flutter#77454) (flutter#79061)
1 parent 3117069 commit b070ed3

File tree

8 files changed

+224
-133
lines changed

8 files changed

+224
-133
lines changed

dev/integration_tests/web_e2e_tests/test_driver/text_editing_integration.dart

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
// found in the LICENSE file.
44

55
// @dart = 2.9
6+
67
import 'dart:html';
78
import 'dart:js_util' as js_util;
9+
810
import 'package:flutter/gestures.dart';
911
import 'package:flutter/services.dart';
1012
import 'package:flutter_test/flutter_test.dart';
@@ -21,9 +23,6 @@ void main() {
2123
app.main();
2224
await tester.pumpAndSettle();
2325

24-
// TODO(nurhan): https://github.com/flutter/flutter/issues/51885
25-
SystemChannels.textInput.setMockMethodCallHandler(null);
26-
2726
// Focus on a TextFormField.
2827
final Finder finder = find.byKey(const Key('input'));
2928
expect(finder, findsOneWidget);
@@ -49,9 +48,6 @@ void main() {
4948
app.main();
5049
await tester.pumpAndSettle();
5150

52-
// TODO(nurhan): https://github.com/flutter/flutter/issues/51885
53-
SystemChannels.textInput.setMockMethodCallHandler(null);
54-
5551
// Focus on a TextFormField.
5652
final Finder finder = find.byKey(const Key('empty-input'));
5753
expect(finder, findsOneWidget);
@@ -77,9 +73,6 @@ void main() {
7773
app.main();
7874
await tester.pumpAndSettle();
7975

80-
// TODO(nurhan): https://github.com/flutter/flutter/issues/51885
81-
SystemChannels.textInput.setMockMethodCallHandler(null);
82-
8376
// This text will show no-enter initially. It will have 'enter-pressed'
8477
// after `onFieldSubmitted` of TextField is triggered.
8578
final Finder textFinder = find.byKey(const Key('text'));
@@ -113,9 +106,6 @@ void main() {
113106
app.main();
114107
await tester.pumpAndSettle();
115108

116-
// TODO(nurhan): https://github.com/flutter/flutter/issues/51885
117-
SystemChannels.textInput.setMockMethodCallHandler(null);
118-
119109
// Focus on a TextFormField.
120110
final Finder finder = find.byKey(const Key('input'));
121111
expect(finder, findsOneWidget);
@@ -148,9 +138,6 @@ void main() {
148138
app.main();
149139
await tester.pumpAndSettle();
150140

151-
// TODO(nurhan): https://github.com/flutter/flutter/issues/51885
152-
SystemChannels.textInput.setMockMethodCallHandler(null);
153-
154141
// Focus on a TextFormField.
155142
final Finder finder = find.byKey(const Key('input'));
156143
expect(finder, findsOneWidget);
@@ -198,9 +185,6 @@ void main() {
198185
app.main();
199186
await tester.pumpAndSettle();
200187

201-
// TODO(nurhan): https://github.com/flutter/flutter/issues/51885
202-
SystemChannels.textInput.setMockMethodCallHandler(null);
203-
204188
// Select something from the selectable text.
205189
final Finder finder = find.byKey(const Key('selectable'));
206190
expect(finder, findsOneWidget);

packages/flutter/lib/src/services/system_channels.dart

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ class SystemChannels {
120120
/// they apply, so that stale messages referencing past transactions can be
121121
/// ignored.
122122
///
123+
/// In debug builds, messages sent with a client ID of -1 are always accepted.
124+
/// This allows tests to smuggle messages without having to mock the engine's
125+
/// text handling (for example, allowing the engine to still handle the text
126+
/// input messages in an integration test).
127+
///
123128
/// The methods described below are wrapped in a more convenient form by the
124129
/// [TextInput] and [TextInputConnection] class.
125130
///
@@ -152,9 +157,15 @@ class SystemChannels {
152157
/// is a transaction identifier. Calls for stale transactions should be ignored.
153158
///
154159
/// * `TextInputClient.updateEditingState`: The user has changed the contents
155-
/// of the text control. The second argument is a [String] containing a
156-
/// JSON-encoded object with seven keys, in the form expected by
157-
/// [TextEditingValue.fromJSON].
160+
/// of the text control. The second argument is an object with seven keys,
161+
/// in the form expected by [TextEditingValue.fromJSON].
162+
///
163+
/// * `TextInputClient.updateEditingStateWithTag`: One or more text controls
164+
/// were autofilled by the platform's autofill service. The first argument
165+
/// (the client ID) is ignored, the second argument is a map of tags to
166+
/// objects in the form expected by [TextEditingValue.fromJSON]. See
167+
/// [AutofillScope.getAutofillClient] for details on the interpretation of
168+
/// the tag.
158169
///
159170
/// * `TextInputClient.performAction`: The user has triggered an action. The
160171
/// second argument is a [String] consisting of the stringification of one
@@ -165,7 +176,8 @@ class SystemChannels {
165176
/// one. The framework should call `TextInput.setClient` and
166177
/// `TextInput.setEditingState` again with its most recent information. If
167178
/// there is no existing state on the framework side, the call should
168-
/// fizzle.
179+
/// fizzle. (This call is made without a client ID; indeed, without any
180+
/// arguments at all.)
169181
///
170182
/// * `TextInputClient.onConnectionClosed`: The text input connection closed
171183
/// on the platform side. For example the application is moved to

packages/flutter/lib/src/services/text_input.dart

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,9 +1327,11 @@ class TextInput {
13271327

13281328
final List<dynamic> args = methodCall.arguments as List<dynamic>;
13291329

1330+
// The updateEditingStateWithTag request (autofill) can come up even to a
1331+
// text field that doesn't have a connection.
13301332
if (method == 'TextInputClient.updateEditingStateWithTag') {
1333+
assert(_currentConnection!._client != null);
13311334
final TextInputClient client = _currentConnection!._client;
1332-
assert(client != null);
13331335
final AutofillScope? scope = client.currentAutofillScope;
13341336
final Map<String, dynamic> editingValue = args[1] as Map<String, dynamic>;
13351337
for (final String tag in editingValue.keys) {
@@ -1343,9 +1345,22 @@ class TextInput {
13431345
}
13441346

13451347
final int client = args[0] as int;
1346-
// The incoming message was for a different client.
1347-
if (client != _currentConnection!._id)
1348-
return;
1348+
if (client != _currentConnection!._id) {
1349+
// If the client IDs don't match, the incoming message was for a different
1350+
// client.
1351+
bool debugAllowAnyway = false;
1352+
assert(() {
1353+
// In debug builds we allow "-1" as a magical client ID that ignores
1354+
// this verification step so that tests can always get through, even
1355+
// when they are not mocking the engine side of text input.
1356+
if (client == -1)
1357+
debugAllowAnyway = true;
1358+
return true;
1359+
}());
1360+
if (!debugAllowAnyway)
1361+
return;
1362+
}
1363+
13491364
switch (method) {
13501365
case 'TextInputClient.updateEditingState':
13511366
_currentConnection!._client.updateEditingValue(TextEditingValue.fromJSON(args[1] as Map<String, dynamic>));

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2111,7 +2111,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
21112111
if (_hasFocus) {
21122112
_openInputConnection();
21132113
} else {
2114-
widget.focusNode.requestFocus();
2114+
widget.focusNode.requestFocus(); // This eventually calls _openInputConnection also, see _handleFocusChanged.
21152115
}
21162116
}
21172117

packages/flutter_test/lib/src/binding.dart

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,15 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
195195

196196
/// Called by the test framework at the beginning of a widget test to
197197
/// prepare the binding for the next test.
198+
///
199+
/// If [registerTestTextInput] returns true when this method is called,
200+
/// the [testTextInput] is configured to simulate the keyboard.
198201
void reset() {
199202
_restorationManager = null;
200203
resetGestureBinding();
204+
testTextInput.reset();
205+
if (registerTestTextInput)
206+
_testTextInput.register();
201207
}
202208

203209
@override
@@ -237,14 +243,28 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
237243
@protected
238244
bool get overrideHttpClient => true;
239245

240-
/// Determines whether the binding automatically registers [testTextInput].
246+
/// Determines whether the binding automatically registers [testTextInput] as
247+
/// a fake keyboard implementation.
241248
///
242249
/// Unit tests make use of this to mock out text input communication for
243250
/// widgets. An integration test would set this to false, to test real IME
244251
/// or keyboard input.
245252
///
246253
/// [TestTextInput.isRegistered] reports whether the text input mock is
247254
/// registered or not.
255+
///
256+
/// Some of the properties and methods on [testTextInput] are only valid if
257+
/// [registerTestTextInput] returns true when a test starts. If those
258+
/// members are accessed when using a binding that sets this flag to false,
259+
/// they will throw.
260+
///
261+
/// If this property returns true when a test ends, the [testTextInput] is
262+
/// unregistered.
263+
///
264+
/// This property should not change the value it returns during the lifetime
265+
/// of the binding. Changing the value of this property risks very confusing
266+
/// behavior as the [TestTextInput] may be inconsistently registered or
267+
/// unregistered.
248268
@protected
249269
bool get registerTestTextInput => true;
250270

@@ -319,9 +339,6 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
319339
binding.setupHttpOverrides();
320340
}
321341
_testTextInput = TestTextInput(onCleared: _resetFocusedEditable);
322-
if (registerTestTextInput) {
323-
_testTextInput.register();
324-
}
325342
}
326343

327344
@override
@@ -515,12 +532,20 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
515532
TestTextInput get testTextInput => _testTextInput;
516533
late TestTextInput _testTextInput;
517534

518-
/// The current client of the onscreen keyboard. Callers must pump
519-
/// an additional frame after setting this property to complete the
520-
/// focus change.
535+
/// The [State] of the current [EditableText] client of the onscreen keyboard.
536+
///
537+
/// Setting this property to a new value causes the given [EditableTextState]
538+
/// to focus itself and request the keyboard to establish a
539+
/// [TextInputConnection].
540+
///
541+
/// Callers must pump an additional frame after setting this property to
542+
/// complete the focus change.
521543
///
522544
/// Instead of setting this directly, consider using
523545
/// [WidgetTester.showKeyboard].
546+
//
547+
// TODO(ianh): We should just remove this property and move the call to
548+
// requestKeyboard to the WidgetTester.showKeyboard method.
524549
EditableTextState? get focusedEditable => _focusedEditable;
525550
EditableTextState? _focusedEditable;
526551
set focusedEditable(EditableTextState? value) {
@@ -799,6 +824,8 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
799824
// alone so that we don't cause more spurious errors.
800825
runApp(Container(key: UniqueKey(), child: _postTestMessage)); // Unmount any remaining widgets.
801826
await pump();
827+
if (registerTestTextInput)
828+
_testTextInput.unregister();
802829
invariantTester();
803830
_verifyAutoUpdateGoldensUnset(autoUpdateGoldensBeforeTest && !isBrowser);
804831
_verifyReportTestExceptionUnset(reportTestExceptionBeforeTest);

0 commit comments

Comments
 (0)