diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index cbfe083e6c..400d1a4488 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -293,7 +293,7 @@ class CupertinoTextField extends StatefulWidget { this.onTap, this.scrollController, this.scrollPhysics, - this.autofillHints, + this.autofillHints = const [], this.restorationId, this.enableIMEPersonalizedLearning = true, }) : assert(textAlign != null), @@ -449,7 +449,7 @@ class CupertinoTextField extends StatefulWidget { this.onTap, this.scrollController, this.scrollPhysics, - this.autofillHints, + this.autofillHints = const [], this.restorationId, this.enableIMEPersonalizedLearning = true, }) : assert(textAlign != null), @@ -837,7 +837,7 @@ class CupertinoTextField extends StatefulWidget { } } -class _CupertinoTextFieldState extends State with RestorationMixin, AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate { +class _CupertinoTextFieldState extends State with RestorationMixin, AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient { final GlobalKey _clearGlobalKey = GlobalKey(); RestorableTextEditingController? _controller; @@ -1098,6 +1098,28 @@ class _CupertinoTextFieldState extends State with Restoratio }, ); } + // AutofillClient implementation start. + @override + String get autofillId => _editableText.autofillId; + + @override + void autofill(TextEditingValue newEditingValue) => _editableText.autofill(newEditingValue); + + @override + TextInputConfiguration get textInputConfiguration { + final List? autofillHints = widget.autofillHints?.toList(growable: false); + final AutofillConfiguration autofillConfiguration = autofillHints != null + ? AutofillConfiguration( + uniqueIdentifier: autofillId, + autofillHints: autofillHints, + currentEditingValue: _effectiveController.value, + hintText: widget.placeholder, + ) + : AutofillConfiguration.disabled; + + return _editableText.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration); + } + // AutofillClient implementation end. @override Widget build(BuildContext context) { @@ -1242,7 +1264,7 @@ class _CupertinoTextFieldState extends State with Restoratio scrollController: widget.scrollController, scrollPhysics: widget.scrollPhysics, enableInteractiveSelection: widget.enableInteractiveSelection, - autofillHints: widget.autofillHints, + autofillClient: this, restorationId: 'editable', enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, ), diff --git a/packages/flutter/lib/src/material/selectable_text.dart b/packages/flutter/lib/src/material/selectable_text.dart index 17f6a6b9e8..21e0667ea8 100644 --- a/packages/flutter/lib/src/material/selectable_text.dart +++ b/packages/flutter/lib/src/material/selectable_text.dart @@ -692,6 +692,7 @@ class _SelectableTextState extends State with AutomaticKeepAlive enableInteractiveSelection: widget.enableInteractiveSelection, dragStartBehavior: widget.dragStartBehavior, scrollPhysics: widget.scrollPhysics, + autofillHints: null, ), ); diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index dc712fecdf..caa6c63c90 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -345,7 +345,7 @@ class TextField extends StatefulWidget { this.buildCounter, this.scrollController, this.scrollPhysics, - this.autofillHints, + this.autofillHints = const [], this.restorationId, this.enableIMEPersonalizedLearning = true, }) : assert(textAlign != null), @@ -827,7 +827,7 @@ class TextField extends StatefulWidget { } } -class _TextFieldState extends State with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate { +class _TextFieldState extends State with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient { RestorableTextEditingController? _controller; TextEditingController get _effectiveController => widget.controller ?? _controller!.value; @@ -1094,6 +1094,29 @@ class _TextFieldState extends State with RestorationMixin implements } } + // AutofillClient implementation start. + @override + String get autofillId => _editableText!.autofillId; + + @override + void autofill(TextEditingValue newEditingValue) => _editableText!.autofill(newEditingValue); + + @override + TextInputConfiguration get textInputConfiguration { + final List? autofillHints = widget.autofillHints?.toList(growable: false); + final AutofillConfiguration autofillConfiguration = autofillHints != null + ? AutofillConfiguration( + uniqueIdentifier: autofillId, + autofillHints: autofillHints, + currentEditingValue: _effectiveController.value, + hintText: (widget.decoration ?? const InputDecoration()).hintText, + ) + : AutofillConfiguration.disabled; + + return _editableText!.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration); + } + // AutofillClient implementation end. + @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); @@ -1240,7 +1263,7 @@ class _TextFieldState extends State with RestorationMixin implements dragStartBehavior: widget.dragStartBehavior, scrollController: widget.scrollController, scrollPhysics: widget.scrollPhysics, - autofillHints: widget.autofillHints, + autofillClient: this, autocorrectionTextRectColor: autocorrectionTextRectColor, restorationId: 'editable', enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, diff --git a/packages/flutter/lib/src/services/autofill.dart b/packages/flutter/lib/src/services/autofill.dart index 4a470154c0..65de3fc136 100644 --- a/packages/flutter/lib/src/services/autofill.dart +++ b/packages/flutter/lib/src/services/autofill.dart @@ -631,12 +631,40 @@ class AutofillConfiguration { /// Creates autofill related configuration information that can be sent to the /// platform. const AutofillConfiguration({ + required String uniqueIdentifier, + required List autofillHints, + required TextEditingValue currentEditingValue, + String? hintText, + }) : this._( + enabled: true, + uniqueIdentifier: uniqueIdentifier, + autofillHints: autofillHints, + currentEditingValue: currentEditingValue, + hintText: hintText, + ); + + const AutofillConfiguration._({ + required this.enabled, required this.uniqueIdentifier, - required this.autofillHints, + this.autofillHints = const [], + this.hintText, required this.currentEditingValue, }) : assert(uniqueIdentifier != null), assert(autofillHints != null); + /// An [AutofillConfiguration] that indicates the [AutofillClient] does not + /// wish to be autofilled. + static const AutofillConfiguration disabled = AutofillConfiguration._( + enabled: false, + uniqueIdentifier: '', + currentEditingValue: TextEditingValue.empty, + ); + + /// Whether autofill should be enabled for the [AutofillClient]. + /// + /// To retrieve a disabled [AutofillConfiguration], use [disabled]. + final bool enabled; + /// A string that uniquely identifies the current [AutofillClient]. /// /// The identifier needs to be unique within the [AutofillScope] for the @@ -648,7 +676,7 @@ class AutofillConfiguration { /// A list of strings that helps the autofill service identify the type of the /// [AutofillClient]. /// - /// Must not be null or empty. + /// Must not be null. /// /// {@template flutter.services.AutofillConfiguration.autofillHints} /// For the best results, hint strings need to be understood by the platform's @@ -697,14 +725,23 @@ class AutofillConfiguration { /// The current [TextEditingValue] of the [AutofillClient]. final TextEditingValue currentEditingValue; + /// The optional hint text placed on the view that typically suggests what + /// sort of input the field accepts, for example "enter your password here". + /// + /// If the developer does not specify any [autofillHints], the [hintText] can + /// be a useful indication to the platform autofill service. + final String? hintText; + /// Returns a representation of this object as a JSON object. - Map toJson() { - assert(autofillHints.isNotEmpty); - return { - 'uniqueIdentifier': uniqueIdentifier, - 'hints': autofillHints, - 'editingValue': currentEditingValue.toJSON(), - }; + Map? toJson() { + return enabled + ? { + 'uniqueIdentifier': uniqueIdentifier, + 'hints': autofillHints, + 'editingValue': currentEditingValue.toJSON(), + if (hintText != null) 'hintText': hintText, + } + : null; } } @@ -715,7 +752,7 @@ class AutofillConfiguration { abstract class AutofillClient { /// The unique identifier of this [AutofillClient]. /// - /// Must not be null. + /// Must not be null and the identifier must not be changed. String get autofillId; /// The [TextInputConfiguration] that describes this [AutofillClient]. @@ -726,7 +763,7 @@ abstract class AutofillClient { /// Requests this [AutofillClient] update its [TextEditingValue] to the given /// value. - void updateEditingValue(TextEditingValue newEditingValue); + void autofill(TextEditingValue newEditingValue); } /// An ordered group within which [AutofillClient]s are logically connected. @@ -806,7 +843,7 @@ mixin AutofillScopeMixin implements AutofillScope { TextInputConnection attach(TextInputClient trigger, TextInputConfiguration configuration) { assert(trigger != null); assert( - !autofillClients.any((AutofillClient client) => client.textInputConfiguration.autofillConfiguration == null), + !autofillClients.any((AutofillClient client) => !client.textInputConfiguration.autofillConfiguration.enabled), 'Every client in AutofillScope.autofillClients must enable autofill', ); diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart index c19d856fd8..97cf936302 100644 --- a/packages/flutter/lib/src/services/text_input.dart +++ b/packages/flutter/lib/src/services/text_input.dart @@ -466,7 +466,7 @@ class TextInputConfiguration { this.inputAction = TextInputAction.done, this.keyboardAppearance = Brightness.light, this.textCapitalization = TextCapitalization.none, - this.autofillConfiguration, + this.autofillConfiguration = AutofillConfiguration.disabled, this.enableIMEPersonalizedLearning = true, }) : assert(inputType != null), assert(obscureText != null), @@ -503,7 +503,7 @@ class TextInputConfiguration { /// to the platform. This will prevent the corresponding input field from /// participating in autofills triggered by other fields. Additionally, on /// Android and web, setting [autofillConfiguration] to null disables autofill. - final AutofillConfiguration? autofillConfiguration; + final AutofillConfiguration autofillConfiguration; /// {@template flutter.services.TextInputConfiguration.smartDashesType} /// Whether to allow the platform to automatically format dashes. @@ -607,8 +607,41 @@ class TextInputConfiguration { /// {@endtemplate} final bool enableIMEPersonalizedLearning; + /// Creates a copy of this [TextInputConfiguration] with the given fields + /// replaced with new values. + TextInputConfiguration copyWith({ + TextInputType? inputType, + bool? readOnly, + bool? obscureText, + bool? autocorrect, + SmartDashesType? smartDashesType, + SmartQuotesType? smartQuotesType, + bool? enableSuggestions, + String? actionLabel, + TextInputAction? inputAction, + Brightness? keyboardAppearance, + TextCapitalization? textCapitalization, + bool? enableIMEPersonalizedLearning, + AutofillConfiguration? autofillConfiguration, + }) { + return TextInputConfiguration( + inputType: inputType ?? this.inputType, + readOnly: readOnly ?? this.readOnly, + obscureText: obscureText ?? this.obscureText, + autocorrect: autocorrect ?? this.autocorrect, + smartDashesType: smartDashesType ?? this.smartDashesType, + smartQuotesType: smartQuotesType ?? this.smartQuotesType, + enableSuggestions: enableSuggestions ?? this.enableSuggestions, + inputAction: inputAction ?? this.inputAction, + textCapitalization: textCapitalization ?? this.textCapitalization, + keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance, + enableIMEPersonalizedLearning: enableIMEPersonalizedLearning?? this.enableIMEPersonalizedLearning, + autofillConfiguration: autofillConfiguration ?? this.autofillConfiguration, + ); + } /// Returns a representation of this object as a JSON object. Map toJson() { + final Map? autofill = autofillConfiguration.toJson(); return { 'inputType': inputType.toJson(), 'readOnly': readOnly, @@ -622,7 +655,7 @@ class TextInputConfiguration { 'textCapitalization': textCapitalization.toString(), 'keyboardAppearance': keyboardAppearance.toString(), 'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning, - if (autofillConfiguration != null) 'autofill': autofillConfiguration!.toJson(), + if (autofill != null) 'autofill': autofill, }; } } @@ -1470,7 +1503,10 @@ class TextInput { final TextEditingValue textEditingValue = TextEditingValue.fromJSON( editingValue[tag] as Map, ); - scope?.getAutofillClient(tag)?.updateEditingValue(textEditingValue); + final AutofillClient? client = scope?.getAutofillClient(tag); + if (client != null && client.textInputConfiguration.autofillConfiguration.enabled) { + client.autofill(textEditingValue); + } } return; diff --git a/packages/flutter/lib/src/widgets/autofill.dart b/packages/flutter/lib/src/widgets/autofill.dart index 40b8647e63..d10569946c 100644 --- a/packages/flutter/lib/src/widgets/autofill.dart +++ b/packages/flutter/lib/src/widgets/autofill.dart @@ -134,7 +134,7 @@ class AutofillGroupState extends State with AutofillScopeMixin { @override Iterable get autofillClients { return _clients.values - .where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration != null); + .where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration.enabled); } /// Adds the [AutofillClient] to this [AutofillGroup]. @@ -155,9 +155,8 @@ class AutofillGroupState extends State with AutofillScopeMixin { /// Removes an [AutofillClient] with the given `autofillId` from this /// [AutofillGroup]. /// - /// Typically, this should be called by autofillable [TextInputClient]s in - /// [State.dispose] and [State.didChangeDependencies], when the input field - /// needs to be removed from the [AutofillGroup] it is currently registered to. + /// Typically, this should be called by a text field when it's being disposed, + /// or before it's registered with a different [AutofillGroup]. /// /// See also: /// diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 6a657d38aa..566b23395d 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -456,7 +456,8 @@ class EditableText extends StatefulWidget { paste: true, selectAll: true, ), - this.autofillHints, + this.autofillHints = const [], + this.autofillClient, this.clipBehavior = Clip.hardEdge, this.restorationId, this.scrollBehavior, @@ -499,10 +500,6 @@ class EditableText extends StatefulWidget { assert(dragStartBehavior != null), assert(toolbarOptions != null), assert(clipBehavior != null), - assert( - !readOnly || autofillHints == null, - "Read-only fields can't have autofill hints.", - ), assert(enableIMEPersonalizedLearning != null), _strutStyle = strutStyle, keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines), @@ -1163,15 +1160,17 @@ class EditableText extends StatefulWidget { /// A list of strings that helps the autofill service identify the type of this /// text input. /// - /// When set to null or empty, this text input will not send its autofill - /// information to the platform, preventing it from participating in - /// autofills triggered by a different [AutofillClient], even if they're in the - /// same [AutofillScope]. Additionally, on Android and web, setting this to - /// null or empty will disable autofill for this text field. + /// When set to null, this text input will not send its autofill information + /// to the platform, preventing it from participating in autofills triggered + /// by a different [AutofillClient], even if they're in the same + /// [AutofillScope]. Additionally, on Android and web, setting this to null + /// will disable autofill for this text field. /// /// The minimum platform SDK version that supports Autofill is API level 26 /// for Android, and iOS 10.0 for iOS. /// + /// Defaults to an empty list. + /// /// ### Setting up iOS autofill: /// /// To provide the best user experience and ensure your app fully supports @@ -1229,6 +1228,12 @@ class EditableText extends StatefulWidget { /// {@macro flutter.services.AutofillConfiguration.autofillHints} final Iterable? autofillHints; + /// The [AutofillClient] that controls this input field's autofill behavior. + /// + /// When null, this widget's [EditableTextState] will be used as the + /// [AutofillClient]. This property may override [autofillHints]. + final AutofillClient? autofillClient; + /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge]. @@ -1278,12 +1283,11 @@ class EditableText extends StatefulWidget { required Iterable? autofillHints, required int? maxLines, }) { - if (autofillHints?.isEmpty ?? true) { + if (autofillHints == null || autofillHints.isEmpty) { return maxLines == 1 ? TextInputType.text : TextInputType.multiline; } - TextInputType? returnValue; - final String effectiveHint = autofillHints!.first; + final String effectiveHint = autofillHints.first; // On iOS oftentimes specifying a text content type is not enough to qualify // the input field for autofill. The keyboard type also needs to be compatible @@ -1328,7 +1332,10 @@ class EditableText extends StatefulWidget { AutofillHints.username : TextInputType.text, }; - returnValue = iOSKeyboardType[effectiveHint]; + final TextInputType? keyboardType = iOSKeyboardType[effectiveHint]; + if (keyboardType != null) { + return keyboardType; + } break; case TargetPlatform.android: case TargetPlatform.fuchsia: @@ -1338,8 +1345,9 @@ class EditableText extends StatefulWidget { } } - if (returnValue != null || maxLines != 1) - return returnValue ?? TextInputType.multiline; + if (maxLines != 1) { + return TextInputType.multiline; + } const Map inferKeyboardType = { AutofillHints.addressCity : TextInputType.streetAddress, @@ -1474,8 +1482,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien @override AutofillScope? get currentAutofillScope => _currentAutofillScope; - // Is this field in the current autofill context. - bool _isInAutofillContext = false; + AutofillClient get _effectiveAutofillClient => widget.autofillClient ?? this; /// Whether to create an input connection with the platform for text editing /// or not. @@ -1548,8 +1555,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien if (currentAutofillScope != newAutofillGroup) { _currentAutofillScope?.unregister(autofillId); _currentAutofillScope = newAutofillGroup; - newAutofillGroup?.register(this); - _isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext; + _currentAutofillScope?.register(_effectiveAutofillClient); } if (!_didAutoFocus && widget.autofocus) { @@ -1574,7 +1580,11 @@ class EditableTextState extends State with AutomaticKeepAliveClien _selectionOverlay?.update(_value); } _selectionOverlay?.handlesVisible = widget.showSelectionHandles; - _isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext; + + if (widget.autofillClient != oldWidget.autofillClient) { + _currentAutofillScope?.unregister(oldWidget.autofillClient?.autofillId ?? autofillId); + _currentAutofillScope?.register(_effectiveAutofillClient); + } if (widget.focusNode != oldWidget.focusNode) { oldWidget.focusNode.removeListener(_handleFocusChanged); @@ -1597,7 +1607,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien if (kIsWeb && _hasInputConnection) { if (oldWidget.readOnly != widget.readOnly) { - _textInputConnection!.updateConfig(textInputConfiguration); + _textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration); } } @@ -1980,8 +1990,9 @@ class EditableTextState extends State with AutomaticKeepAliveClien } bool get _hasInputConnection => _textInputConnection?.attached ?? false; - bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? false; - bool get _shouldBeInAutofillContext => _needsAutofill && currentAutofillScope != null; + /// Whether to send the autofill information to the autofill service. True by + /// default. + bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? true; void _openInputConnection() { if (!_shouldCreateInputConnection) { @@ -1999,8 +2010,8 @@ class EditableTextState extends State with AutomaticKeepAliveClien // notified to exclude this field from the autofill context. So we need to // provide the autofillId. _textInputConnection = _needsAutofill && currentAutofillScope != null - ? currentAutofillScope!.attach(this, textInputConfiguration) - : TextInput.attach(this, _createTextInputConfiguration(_isInAutofillContext || _needsAutofill)); + ? currentAutofillScope!.attach(this, _effectiveAutofillClient.textInputConfiguration) + : TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration); _textInputConnection!.show(); _updateSizeAndTransform(); _updateComposingRectIfNeeded(); @@ -2523,8 +2534,17 @@ class EditableTextState extends State with AutomaticKeepAliveClien @override String get autofillId => 'EditableText-$hashCode'; - TextInputConfiguration _createTextInputConfiguration(bool needsAutofillConfiguration) { - assert(needsAutofillConfiguration != null); + @override + TextInputConfiguration get textInputConfiguration { + final List? autofillHints = widget.autofillHints?.toList(growable: false); + final AutofillConfiguration autofillConfiguration = autofillHints != null + ? AutofillConfiguration( + uniqueIdentifier: autofillId, + autofillHints: autofillHints, + currentEditingValue: currentTextEditingValue, + ) + : AutofillConfiguration.disabled; + return TextInputConfiguration( inputType: widget.keyboardType, readOnly: widget.readOnly, @@ -2539,19 +2559,13 @@ class EditableTextState extends State with AutomaticKeepAliveClien ), textCapitalization: widget.textCapitalization, keyboardAppearance: widget.keyboardAppearance, - autofillConfiguration: !needsAutofillConfiguration ? null : AutofillConfiguration( - uniqueIdentifier: autofillId, - autofillHints: widget.autofillHints?.toList(growable: false) ?? [], - currentEditingValue: currentTextEditingValue, - ), + autofillConfiguration: autofillConfiguration, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, ); } @override - TextInputConfiguration get textInputConfiguration { - return _createTextInputConfiguration(_needsAutofill); - } + void autofill(TextEditingValue value) => updateEditingValue(value); // null if no promptRect should be shown. TextRange? _currentPromptRectRange; diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index d35379755e..5af02d563f 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -4776,6 +4776,22 @@ void main() { }, ); + testWidgets('autofill info has placeholder text', (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: CupertinoTextField( + placeholder: 'placeholder text', + ), + ), + ); + await tester.tap(find.byType(CupertinoTextField)); + + expect( + tester.testTextInput.setClientArgs?['autofill'], + containsPair('hintText', 'placeholder text'), + ); + }); + testWidgets('textDirection is passed to EditableText', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 02bc2d0bc8..e6b0518368 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -9924,4 +9924,27 @@ void main() { expect(prefixTapCount, 1); expect(suffixTapCount, 1); }); + + testWidgets('autofill info has hint text', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: Center( + child: TextField( + decoration: InputDecoration( + hintText: 'placeholder text' + ), + ), + ), + ), + ), + ); + + await tester.tap(find.byType(TextField)); + + expect( + tester.testTextInput.setClientArgs?['autofill'], + containsPair('hintText', 'placeholder text'), + ); + }); } diff --git a/packages/flutter/test/services/autofill_test.dart b/packages/flutter/test/services/autofill_test.dart index 5af15da712..0dfdf1d5b4 100644 --- a/packages/flutter/test/services/autofill_test.dart +++ b/packages/flutter/test/services/autofill_test.dart @@ -10,7 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('TextInput message channels', () { + group('AutofillClient', () { late FakeTextChannel fakeTextChannel; final FakeAutofillScope scope = FakeAutofillScope(); @@ -25,21 +25,19 @@ void main() { TextInput.setChannel(SystemChannels.textInput); }); - test('throws if the hint list is empty', () async { - Map? json; + test('Does not throw if the hint list is empty', () async { + Object? exception; try { - const AutofillConfiguration config = AutofillConfiguration( + const AutofillConfiguration( uniqueIdentifier: 'id', autofillHints: [], currentEditingValue: TextEditingValue.empty, ); - - json = config.toJson(); } catch (e) { - expect(e.toString(), contains('isNotEmpty')); + exception = e; } - expect(json, isNull); + expect(exception, isNull); }); test( @@ -140,6 +138,9 @@ class FakeAutofillClient implements TextInputClient, AutofillClient { void showAutocorrectionPromptRect(int start, int end) { latestMethodCall = 'showAutocorrectionPromptRect'; } + + @override + void autofill(TextEditingValue newEditingValue) => updateEditingValue(newEditingValue); } class FakeAutofillScope with AutofillScopeMixin implements AutofillScope { diff --git a/packages/flutter/test/widgets/autofill_group_test.dart b/packages/flutter/test/widgets/autofill_group_test.dart index d61174501a..6e7dc3fe78 100644 --- a/packages/flutter/test/widgets/autofill_group_test.dart +++ b/packages/flutter/test/widgets/autofill_group_test.dart @@ -25,7 +25,7 @@ void main() { client1, AutofillGroup( key: innerKey, - child: Column(children: const [client2, TextField()]), + child: Column(children: const [client2, TextField(autofillHints: null)]), ), ]), ), @@ -36,23 +36,19 @@ void main() { final AutofillGroupState innerState = tester.state(find.byKey(innerKey)); final AutofillGroupState outerState = tester.state(find.byKey(outerKey)); - final EditableTextState clientState1 = tester.state( - find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)), - ); - final EditableTextState clientState2 = tester.state( - find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)), - ); + final State clientState1 = tester.state>(find.byWidget(client1)); + final State clientState2 = tester.state>(find.byWidget(client2)); - expect(outerState.autofillClients, [clientState1]); - // The second TextField doesn't have autofill enabled. - expect(innerState.autofillClients, [clientState2]); + expect(outerState.autofillClients.toList(), >[clientState1]); + // The second TextField in the AutofillGroup doesn't have autofill enabled. + expect(innerState.autofillClients.toList(), >[clientState2]); }); testWidgets('new clients can be added & removed to a scope', (WidgetTester tester) async { const Key scopeKey = Key('scope'); const TextField client1 = TextField(autofillHints: ['1']); - TextField client2 = const TextField(autofillHints: []); + TextField client2 = const TextField(autofillHints: null); late StateSetter setState; @@ -74,14 +70,10 @@ void main() { final AutofillGroupState scopeState = tester.state(find.byKey(scopeKey)); - final EditableTextState clientState1 = tester.state( - find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)), - ); - final EditableTextState clientState2 = tester.state( - find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)), - ); + final State clientState1 = tester.state>(find.byWidget(client1)); + final State clientState2 = tester.state>(find.byWidget(client2)); - expect(scopeState.autofillClients, [clientState1]); + expect(scopeState.autofillClients.toList(), >[clientState1]); // Add to scope. setState(() { client2 = const TextField(autofillHints: ['2']); }); @@ -93,11 +85,11 @@ void main() { expect(scopeState.autofillClients.length, 2); // Remove from scope again. - setState(() { client2 = const TextField(autofillHints: []); }); + setState(() { client2 = const TextField(autofillHints: null); }); await tester.pump(); - expect(scopeState.autofillClients, [clientState1]); + expect(scopeState.autofillClients, >[clientState1]); }); testWidgets('AutofillGroup has the right clients after reparenting', (WidgetTester tester) async { @@ -131,16 +123,9 @@ void main() { final AutofillGroupState innerState = tester.state(find.byKey(innerKey)); final AutofillGroupState outerState = tester.state(find.byKey(outerKey)); - final EditableTextState clientState1 = tester.state( - find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)), - ); - final EditableTextState clientState2 = tester.state( - find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)), - ); - - final EditableTextState clientState3 = tester.state( - find.descendant(of: find.byKey(keyClient3), matching: find.byType(EditableText)), - ); + final State clientState1 = tester.state>(find.byWidget(client1)); + final State clientState2 = tester.state>(find.byWidget(client2)); + final State clientState3 = tester.state>(find.byKey(keyClient3)); await tester.pumpWidget( MaterialApp( @@ -163,7 +148,7 @@ void main() { expect(outerState.autofillClients.length, 2); expect(outerState.autofillClients, contains(clientState1)); expect(outerState.autofillClients, contains(clientState3)); - expect(innerState.autofillClients, [clientState2]); + expect(innerState.autofillClients, >[clientState2]); }); testWidgets('disposing AutofillGroups', (WidgetTester tester) async { @@ -270,8 +255,7 @@ void main() { // Remove the topmosts group group3. Should commit. setState(() { - children = const [ - ]; + children = const []; }); await tester.pump();