Unverified Commit 33755f20 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

[autofill] opt-out instead of opt-in (#86312)

parent 81142c1f
......@@ -293,7 +293,7 @@ class CupertinoTextField extends StatefulWidget {
this.onTap,
this.scrollController,
this.scrollPhysics,
this.autofillHints,
this.autofillHints = const <String>[],
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 <String>[],
this.restorationId,
this.enableIMEPersonalizedLearning = true,
}) : assert(textAlign != null),
......@@ -837,7 +837,7 @@ class CupertinoTextField extends StatefulWidget {
}
}
class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate {
class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
final GlobalKey _clearGlobalKey = GlobalKey();
RestorableTextEditingController? _controller;
......@@ -1098,6 +1098,28 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> 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<String>? 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<CupertinoTextField> with Restoratio
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
enableInteractiveSelection: widget.enableInteractiveSelection,
autofillHints: widget.autofillHints,
autofillClient: this,
restorationId: 'editable',
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
),
......
......@@ -692,6 +692,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
enableInteractiveSelection: widget.enableInteractiveSelection,
dragStartBehavior: widget.dragStartBehavior,
scrollPhysics: widget.scrollPhysics,
autofillHints: null,
),
);
......
......@@ -345,7 +345,7 @@ class TextField extends StatefulWidget {
this.buildCounter,
this.scrollController,
this.scrollPhysics,
this.autofillHints,
this.autofillHints = const <String>[],
this.restorationId,
this.enableIMEPersonalizedLearning = true,
}) : assert(textAlign != null),
......@@ -827,7 +827,7 @@ class TextField extends StatefulWidget {
}
}
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate {
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
RestorableTextEditingController? _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
......@@ -1094,6 +1094,29 @@ class _TextFieldState extends State<TextField> 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<String>? 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<TextField> with RestorationMixin implements
dragStartBehavior: widget.dragStartBehavior,
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
autofillHints: widget.autofillHints,
autofillClient: this,
autocorrectionTextRectColor: autocorrectionTextRectColor,
restorationId: 'editable',
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
......
......@@ -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<String> 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 <String>[],
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<String, dynamic> toJson() {
assert(autofillHints.isNotEmpty);
return <String, dynamic>{
'uniqueIdentifier': uniqueIdentifier,
'hints': autofillHints,
'editingValue': currentEditingValue.toJSON(),
};
Map<String, dynamic>? toJson() {
return enabled
? <String, dynamic>{
'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',
);
......
......@@ -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<String, dynamic> toJson() {
final Map<String, dynamic>? autofill = autofillConfiguration.toJson();
return <String, dynamic>{
'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<String, dynamic>,
);
scope?.getAutofillClient(tag)?.updateEditingValue(textEditingValue);
final AutofillClient? client = scope?.getAutofillClient(tag);
if (client != null && client.textInputConfiguration.autofillConfiguration.enabled) {
client.autofill(textEditingValue);
}
}
return;
......
......@@ -134,7 +134,7 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
@override
Iterable<AutofillClient> 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<AutofillGroup> 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:
///
......
......@@ -456,7 +456,8 @@ class EditableText extends StatefulWidget {
paste: true,
selectAll: true,
),
this.autofillHints,
this.autofillHints = const <String>[],
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<String>? 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<String>? 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<String, TextInputType> inferKeyboardType = <String, TextInputType> {
AutofillHints.addressCity : TextInputType.streetAddress,
......@@ -1474,8 +1482,7 @@ class EditableTextState extends State<EditableText> 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<EditableText> 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<EditableText> 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<EditableText> 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<EditableText> 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<EditableText> 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<EditableText> with AutomaticKeepAliveClien
@override
String get autofillId => 'EditableText-$hashCode';
TextInputConfiguration _createTextInputConfiguration(bool needsAutofillConfiguration) {
assert(needsAutofillConfiguration != null);
@override
TextInputConfiguration get textInputConfiguration {
final List<String>? 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<EditableText> with AutomaticKeepAliveClien
),
textCapitalization: widget.textCapitalization,
keyboardAppearance: widget.keyboardAppearance,
autofillConfiguration: !needsAutofillConfiguration ? null : AutofillConfiguration(
uniqueIdentifier: autofillId,
autofillHints: widget.autofillHints?.toList(growable: false) ?? <String>[],
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;
......
......@@ -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(
......
......@@ -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'),
);
});
}
......@@ -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<String, dynamic>? json;
test('Does not throw if the hint list is empty', () async {
Object? exception;
try {
const AutofillConfiguration config = AutofillConfiguration(
const AutofillConfiguration(
uniqueIdentifier: 'id',
autofillHints: <String>[],
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 {
......
......@@ -25,7 +25,7 @@ void main() {
client1,
AutofillGroup(
key: innerKey,
child: Column(children: const <Widget>[client2, TextField()]),
child: Column(children: const <Widget>[client2, TextField(autofillHints: null)]),
),
]),
),
......@@ -36,23 +36,19 @@ void main() {
final AutofillGroupState innerState = tester.state<AutofillGroupState>(find.byKey(innerKey));
final AutofillGroupState outerState = tester.state<AutofillGroupState>(find.byKey(outerKey));
final EditableTextState clientState1 = tester.state<EditableTextState>(
find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)),
);
final EditableTextState clientState2 = tester.state<EditableTextState>(
find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)),
);
final State<TextField> clientState1 = tester.state<State<TextField>>(find.byWidget(client1));
final State<TextField> clientState2 = tester.state<State<TextField>>(find.byWidget(client2));
expect(outerState.autofillClients, <EditableTextState>[clientState1]);
// The second TextField doesn't have autofill enabled.
expect(innerState.autofillClients, <EditableTextState>[clientState2]);
expect(outerState.autofillClients.toList(), <State<TextField>>[clientState1]);
// The second TextField in the AutofillGroup doesn't have autofill enabled.
expect(innerState.autofillClients.toList(), <State<TextField>>[clientState2]);
});
testWidgets('new clients can be added & removed to a scope', (WidgetTester tester) async {
const Key scopeKey = Key('scope');
const TextField client1 = TextField(autofillHints: <String>['1']);
TextField client2 = const TextField(autofillHints: <String>[]);
TextField client2 = const TextField(autofillHints: null);
late StateSetter setState;
......@@ -74,14 +70,10 @@ void main() {
final AutofillGroupState scopeState = tester.state<AutofillGroupState>(find.byKey(scopeKey));
final EditableTextState clientState1 = tester.state<EditableTextState>(
find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)),
);
final EditableTextState clientState2 = tester.state<EditableTextState>(
find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)),
);
final State<TextField> clientState1 = tester.state<State<TextField>>(find.byWidget(client1));
final State<TextField> clientState2 = tester.state<State<TextField>>(find.byWidget(client2));
expect(scopeState.autofillClients, <EditableTextState>[clientState1]);
expect(scopeState.autofillClients.toList(), <State<TextField>>[clientState1]);
// Add to scope.
setState(() { client2 = const TextField(autofillHints: <String>['2']); });
......@@ -93,11 +85,11 @@ void main() {
expect(scopeState.autofillClients.length, 2);
// Remove from scope again.
setState(() { client2 = const TextField(autofillHints: <String>[]); });
setState(() { client2 = const TextField(autofillHints: null); });
await tester.pump();
expect(scopeState.autofillClients, <EditableTextState>[clientState1]);
expect(scopeState.autofillClients, <State<TextField>>[clientState1]);
});
testWidgets('AutofillGroup has the right clients after reparenting', (WidgetTester tester) async {
......@@ -131,16 +123,9 @@ void main() {
final AutofillGroupState innerState = tester.state<AutofillGroupState>(find.byKey(innerKey));
final AutofillGroupState outerState = tester.state<AutofillGroupState>(find.byKey(outerKey));
final EditableTextState clientState1 = tester.state<EditableTextState>(
find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)),
);
final EditableTextState clientState2 = tester.state<EditableTextState>(
find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)),
);
final EditableTextState clientState3 = tester.state<EditableTextState>(
find.descendant(of: find.byKey(keyClient3), matching: find.byType(EditableText)),
);
final State<TextField> clientState1 = tester.state<State<TextField>>(find.byWidget(client1));
final State<TextField> clientState2 = tester.state<State<TextField>>(find.byWidget(client2));
final State<TextField> clientState3 = tester.state<State<TextField>>(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, <EditableTextState>[clientState2]);
expect(innerState.autofillClients, <State<TextField>>[clientState2]);
});
testWidgets('disposing AutofillGroups', (WidgetTester tester) async {
......@@ -270,8 +255,7 @@ void main() {
// Remove the topmosts group group3. Should commit.
setState(() {
children = const <Widget> [
];
children = const <Widget> [];
});
await tester.pump();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment