Unverified Commit e31f7089 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Autofill Part 1 (#52126)

parent b7e30cfc
......@@ -11,6 +11,7 @@
library services;
export 'src/services/asset_bundle.dart';
export 'src/services/autofill.dart';
export 'src/services/binary_messenger.dart';
export 'src/services/binding.dart';
export 'src/services/clipboard.dart';
......
......@@ -268,6 +268,7 @@ class CupertinoTextField extends StatefulWidget {
this.onTap,
this.scrollController,
this.scrollPhysics,
this.autofillHints,
}) : assert(textAlign != null),
assert(readOnly != null),
assert(autofocus != null),
......@@ -579,6 +580,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.material.textfield.onTap}
final GestureTapCallback onTap;
/// {@macro flutter.widgets.editableText.autofillHints}
final Iterable<String> autofillHints;
@override
_CupertinoTextFieldState createState() => _CupertinoTextFieldState();
......@@ -950,6 +954,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
enableInteractiveSelection: widget.enableInteractiveSelection,
autofillHints: widget.autofillHints,
),
),
);
......
......@@ -346,6 +346,7 @@ class TextField extends StatefulWidget {
this.buildCounter,
this.scrollController,
this.scrollPhysics,
this.autofillHints,
}) : assert(textAlign != null),
assert(readOnly != null),
assert(autofocus != null),
......@@ -710,6 +711,9 @@ class TextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.scrollController}
final ScrollController scrollController;
/// {@macro flutter.widgets.editableText.autofillHints}
final Iterable<String> autofillHints;
@override
_TextFieldState createState() => _TextFieldState();
......@@ -1049,6 +1053,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
dragStartBehavior: widget.dragStartBehavior,
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
autofillHints: widget.autofillHints,
autocorrectionTextRectColor: autocorrectionTextRectColor,
),
);
......
This diff is collapsed.
......@@ -16,6 +16,7 @@ import 'dart:ui' show
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;
import 'autofill.dart';
import 'message_codec.dart';
import 'platform_channel.dart';
import 'system_channels.dart';
......@@ -441,6 +442,7 @@ class TextInputConfiguration {
this.inputAction = TextInputAction.done,
this.keyboardAppearance = Brightness.light,
this.textCapitalization = TextCapitalization.none,
this.autofillConfiguration,
}) : assert(inputType != null),
assert(obscureText != null),
smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
......@@ -464,6 +466,14 @@ class TextInputConfiguration {
/// Defaults to true.
final bool autocorrect;
/// The configuration to use for autofill.
///
/// Defaults to null, in which case no autofill information will be provided
/// 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;
/// {@template flutter.services.textInput.smartDashesType}
/// Whether to allow the platform to automatically format dashes.
///
......@@ -565,6 +575,7 @@ class TextInputConfiguration {
'inputAction': inputAction.toString(),
'textCapitalization': textCapitalization.toString(),
'keyboardAppearance': keyboardAppearance.toString(),
if (autofillConfiguration != null) 'autofill': autofillConfiguration.toJson(),
};
}
}
......@@ -745,6 +756,21 @@ abstract class TextInputClient {
/// const constructors so that they can be used in const expressions.
const TextInputClient();
/// The current state of the [TextEditingValue] held by this client.
TextEditingValue get currentTextEditingValue;
/// The [AutofillScope] this [TextInputClient] belongs to, if any.
///
/// It should return null if this [TextInputClient] does not need autofill
/// support. For a [TextInputClient] that supports autofill, returning null
/// causes it to participate in autofill alone.
///
/// See also:
///
/// * [AutofillGroup], a widget that creates an [AutofillScope] for its
/// descendent autofillable [TextInputClient]s.
AutofillScope get currentAutofillScope;
/// Requests that this client update its editing state to the given value.
void updateEditingValue(TextEditingValue value);
......@@ -754,9 +780,6 @@ abstract class TextInputClient {
/// Updates the floating cursor position and state.
void updateFloatingCursor(RawFloatingCursorPoint point);
/// The current state of the [TextEditingValue] held by this client.
TextEditingValue get currentTextEditingValue;
/// Requests that this client display a prompt rectangle for the given text range,
/// to indicate the range of text that will be changed by a pending autocorrection.
///
......@@ -809,6 +832,17 @@ class TextInputConnection {
TextInput._instance._show();
}
/// Requests the platform autofill UI to appear.
///
/// The call has no effect unless the currently attached client supports
/// autofill, and the platform has a standalone autofill UI (for example, this
/// call has no effect on iOS since its autofill UI is part of the software
/// keyboard).
void requestAutofill() {
assert(attached);
TextInput._instance._requestAutofill();
}
/// Requests that the text input control change its internal state to match the given state.
void setEditingState(TextEditingValue value) {
assert(attached);
......@@ -1065,6 +1099,22 @@ class TextInput {
}
final List<dynamic> args = methodCall.arguments as List<dynamic>;
if (method == 'TextInputClient.updateEditingStateWithTag') {
final TextInputClient client = _currentConnection._client;
assert(client != null);
final AutofillScope scope = client.currentAutofillScope;
final Map<String, dynamic> editingValue = args[1] as Map<String, dynamic>;
for (final String tag in editingValue.keys) {
final TextEditingValue textEditingValue = TextEditingValue.fromJSON(
editingValue[tag] as Map<String, dynamic>,
);
scope?.getAutofillClient(tag)?.updateEditingValue(textEditingValue);
}
return;
}
final int client = args[0] as int;
// The incoming message was for a different client.
if (client != _currentConnection._id)
......@@ -1128,6 +1178,10 @@ class TextInput {
_channel.invokeMethod<void>('TextInput.show');
}
void _requestAutofill() {
_channel.invokeMethod<void>('TextInput.requestAutofill');
}
void _setEditableSizeAndTransform(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setEditableSizeAndTransform',
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'framework.dart';
export 'package:flutter/services.dart' show AutofillHints;
/// An [AutofillScope] widget that groups [AutofillClient]s together.
///
/// [AutofillClient]s within the same [AutofillScope] must be built together, and
/// they be will be autofilled together.
///
/// {@macro flutter.services.autofill.AutofillScope}
///
/// The [AutofillGroup] widget only knows about [AutofillClient]s registered to
/// it using the [AutofillGroupState.register] API. Typically, [AutofillGroup]
/// will not pick up [AutofillClient]s that are not mounted, for example, an
/// [AutofillClient] within a [Scrollable] that has never been scrolled into the
/// viewport. To workaround this problem, ensure clients in the same [AutofillGroup]
/// are built together:
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// An example form with autofillable fields grouped into different `AutofillGroup`s.
///
/// ```dart
/// bool isSameAddress = true;
/// final TextEditingController shippingAddress1 = TextEditingController();
/// final TextEditingController shippingAddress2 = TextEditingController();
/// final TextEditingController billingAddress1 = TextEditingController();
/// final TextEditingController billingAddress2 = TextEditingController();
///
/// final TextEditingController creditCardNumber = TextEditingController();
/// final TextEditingController creditCardSecurityCode = TextEditingController();
///
/// final TextEditingController phoneNumber = TextEditingController();
///
/// @override
/// Widget build(BuildContext context) {
/// return ListView(
/// children: <Widget>[
/// const Text('Shipping address'),
/// // The address fields are grouped together as some platforms are capable
/// // of autofilling all these fields in one go.
/// AutofillGroup(
/// child: Column(
/// children: <Widget>[
/// TextField(
/// controller: shippingAddress1,
/// autofillHints: <String>[AutofillHints.streetAddressLine1],
/// ),
/// TextField(
/// controller: shippingAddress2,
/// autofillHints: <String>[AutofillHints.streetAddressLine2],
/// ),
/// ],
/// ),
/// ),
/// const Text('Billing address'),
/// Checkbox(
/// value: isSameAddress,
/// onChanged: (bool newValue) {
/// setState(() { isSameAddress = newValue; });
/// },
/// ),
/// // Again the address fields are grouped together for the same reason.
/// if (!isSameAddress) AutofillGroup(
/// child: Column(
/// children: <Widget>[
/// TextField(
/// controller: billingAddress1,
/// autofillHints: <String>[AutofillHints.streetAddressLine1],
/// ),
/// TextField(
/// controller: billingAddress2,
/// autofillHints: <String>[AutofillHints.streetAddressLine2],
/// ),
/// ],
/// ),
/// ),
/// const Text('Credit Card Information'),
/// // The credit card number and the security code are grouped together as
/// // some platforms are capable of autofilling both fields.
/// AutofillGroup(
/// child: Column(
/// children: <Widget>[
/// TextField(
/// controller: creditCardNumber,
/// autofillHints: <String>[AutofillHints.creditCardNumber],
/// ),
/// TextField(
/// controller: creditCardSecurityCode,
/// autofillHints: <String>[AutofillHints.creditCardSecurityCode],
/// ),
/// ],
/// ),
/// ),
/// const Text('Contact Phone Number'),
/// // The phone number field can still be autofilled despite lacking an
/// // `AutofillScope`.
/// TextField(
/// controller: phoneNumber,
/// autofillHints: <String>[AutofillHints.telephoneNumber],
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
class AutofillGroup extends StatefulWidget {
/// Creates a scope for autofillable input fields.
///
/// The [child] argument must not be null.
const AutofillGroup({
Key key,
@required this.child,
}) : assert(child != null),
super(key: key);
/// Returns the closest [AutofillGroupState] which encloses the given context.
///
/// {@macro flutter.widgets.autofill.AutofillGroupState}
///
/// See also:
///
/// * [EditableTextState], where this method is used to retrive the closest
/// [AutofillGroupState].
static AutofillGroupState of(BuildContext context) {
final _AutofillScope scope = context.dependOnInheritedWidgetOfExactType<_AutofillScope>();
return scope?._scope;
}
/// {@macro flutter.widgets.child}
final Widget child;
@override
AutofillGroupState createState() => AutofillGroupState();
}
/// State associated with an [AutofillGroup] widget.
///
/// {@template flutter.widgets.autofill.AutofillGroupState}
/// An [AutofillGroupState] can be used to register an [AutofillClient] when it
/// enters this [AutofillGroup] (for example, when an [EditableText] is mounted or
/// reparented onto the [AutofillGroup]'s subtree), and unregister an
/// [AutofillClient] when it exits (for example, when an [EditableText] gets
/// unmounted or reparented out of the [AutofillGroup]'s subtree).
///
/// The [AutofillGroupState] class also provides an [attach] method that can be
/// called by [TextInputClient]s that support autofill, instead of
/// [TextInputClient.attach], to create a [TextInputConnection] to interact with
/// the platform's text input system.
/// {@endtemplate}
///
/// Typically obtained using [AutofillGroup.of].
class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
final Map<String, AutofillClient> _clients = <String, AutofillClient>{};
@override
AutofillClient getAutofillClient(String tag) => _clients[tag];
@override
Iterable<AutofillClient> get autofillClients {
return _clients.values
.where((AutofillClient client) => client?.textInputConfiguration?.autofillConfiguration != null);
}
/// Adds the [AutofillClient] to this [AutofillGroup].
///
/// Typically, this is called by [TextInputClient]s that support autofill (for
/// example, [EditableTextState]) in [State.didChangeDependencies], when the
/// input field should be registered to a new [AutofillGroup].
///
/// See also:
///
/// * [EditableTextState.didChangeDependencies], where this method is called
/// to update the current [AutofillScope] when needed.
void register(AutofillClient client) {
assert(client != null);
_clients.putIfAbsent(client.autofillId, () => client);
}
/// 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.
///
/// See also:
///
/// * [EditableTextState.didChangeDependencies], where this method is called
/// to unregister from the previous [AutofillScope].
/// * [EditableTextState.dispose], where this method is called to unregister
/// from the current [AutofillScope] when the widget is about to be removed
/// from the tree.
void unregister(String autofillId) {
assert(autofillId != null && _clients.containsKey(autofillId));
_clients.remove(autofillId);
}
@override
Widget build(BuildContext context) {
return _AutofillScope(
autofillScopeState: this,
child: widget.child,
);
}
}
class _AutofillScope extends InheritedWidget {
const _AutofillScope({
Key key,
Widget child,
AutofillGroupState autofillScopeState,
}) : _scope = autofillScopeState,
super(key: key, child: child);
final AutofillGroupState _scope;
AutofillGroup get client => _scope.widget;
@override
bool updateShouldNotify(_AutofillScope old) => _scope != old._scope;
}
......@@ -7,12 +7,13 @@ import 'dart:math' as math;
import 'dart:ui' as ui hide TextStyle;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'autofill.dart';
import 'automatic_keep_alive.dart';
import 'basic.dart';
import 'binding.dart';
......@@ -29,8 +30,8 @@ import 'scrollable.dart';
import 'text_selection.dart';
import 'ticker_provider.dart';
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
export 'package:flutter/rendering.dart' show SelectionChangedCause;
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
/// Signature for the callback that reports when the user changes the selection
/// (including the cursor location).
......@@ -409,6 +410,7 @@ class EditableText extends StatefulWidget {
paste: true,
selectAll: true,
),
this.autofillHints,
}) : assert(controller != null),
assert(focusNode != null),
assert(obscureText != null),
......@@ -1079,6 +1081,23 @@ class EditableText extends StatefulWidget {
/// {@macro flutter.rendering.editable.selectionEnabled}
bool get selectionEnabled => enableInteractiveSelection;
/// {@template flutter.widgets.editableText.autofillHints}
/// A list of strings that helps the autofill service identify the type of this
/// text input.
///
/// When set to null or empty, the text input will not send any autofill related
/// information to the platform. As a result, it will not participate 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.
///
/// The minimum platform SDK version that supports Autofill is API level 26
/// for Android, and iOS 10.0 for iOS.
///
/// {@macro flutter.services.autofill.autofillHints}
/// {@endtemplate}
final Iterable<String> autofillHints;
@override
EditableTextState createState() => EditableTextState();
......@@ -1104,11 +1123,12 @@ class EditableText extends StatefulWidget {
properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null));
properties.add(DiagnosticsProperty<Iterable<String>>('autofillHints', autofillHints, defaultValue: null));
}
}
/// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText> implements TextInputClient, TextSelectionDelegate {
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText> implements TextSelectionDelegate, TextInputClient, AutofillClient {
Timer _cursorTimer;
bool _targetCursorVisibility = false;
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
......@@ -1128,6 +1148,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
bool _didAutoFocus = false;
FocusAttachment _focusAttachment;
AutofillGroupState _currentAutofillScope;
@override
AutofillScope get currentAutofillScope => _currentAutofillScope;
// This value is an eyeball estimation of the time it takes for the iOS cursor
// to ease in and out.
static const Duration _fadeDuration = Duration(milliseconds: 250);
......@@ -1175,6 +1199,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
void didChangeDependencies() {
super.didChangeDependencies();
final AutofillGroupState newAutofillGroup = AutofillGroup.of(context);
if (currentAutofillScope != newAutofillGroup) {
_currentAutofillScope?.unregister(autofillId);
_currentAutofillScope = newAutofillGroup;
newAutofillGroup?.register(this);
}
if (!_didAutoFocus && widget.autofocus) {
_didAutoFocus = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
......@@ -1210,6 +1242,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (oldWidget.readOnly && _hasFocus)
_openInputConnection();
}
if (widget.style != oldWidget.style) {
final TextStyle style = widget.style;
// The _textInputConnection will pick up the new style when it attaches in
......@@ -1228,6 +1261,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
void dispose() {
_currentAutofillScope?.unregister(autofillId);
widget.controller.removeListener(_didChangeTextEditingValue);
_cursorBlinkOpacityController.removeListener(_onCursorColorTick);
_floatingCursorResetController.removeListener(_onFloatingCursorResetTick);
......@@ -1278,10 +1312,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_formatAndSetValue(value);
// To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed.
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
if (_hasInputConnection) {
// To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed.
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
}
}
@override
......@@ -1465,26 +1501,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (!_hasInputConnection) {
final TextEditingValue localValue = _value;
_lastFormattedUnmodifiedTextEditingValue = localValue;
_textInputConnection = TextInput.attach(
this,
TextInputConfiguration(
inputType: widget.keyboardType,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType ?? (widget.obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType: widget.smartQuotesType ?? (widget.obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
enableSuggestions: widget.enableSuggestions,
inputAction: widget.textInputAction ?? (widget.keyboardType == TextInputType.multiline
? TextInputAction.newline
: TextInputAction.done
),
textCapitalization: widget.textCapitalization,
keyboardAppearance: widget.keyboardAppearance,
),
);
_textInputConnection.show();
_textInputConnection = (widget.autofillHints?.isNotEmpty ?? false) && currentAutofillScope != null
? currentAutofillScope.attach(this, textInputConfiguration)
: TextInput.attach(this, textInputConfiguration);
_textInputConnection.show();
_updateSizeAndTransform();
// Request autofill AFTER the size and the transform have been sent to the
// platform side.
_textInputConnection.requestAutofill();
final TextStyle style = widget.style;
_textInputConnection
..setStyle(
......@@ -1904,6 +1930,33 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
@override
String get autofillId => 'EditableText-$hashCode';
@override
TextInputConfiguration get textInputConfiguration {
final bool isAutofillEnabled = widget.autofillHints?.isNotEmpty ?? false;
return TextInputConfiguration(
inputType: widget.keyboardType,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType ?? (widget.obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType: widget.smartQuotesType ?? (widget.obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
enableSuggestions: widget.enableSuggestions,
inputAction: widget.textInputAction ?? (widget.keyboardType == TextInputType.multiline
? TextInputAction.newline
: TextInputAction.done
),
textCapitalization: widget.textCapitalization,
keyboardAppearance: widget.keyboardAppearance,
autofillConfiguration: !isAutofillEnabled ? null : AutofillConfiguration(
uniqueIdentifier: autofillId,
autofillHints: widget.autofillHints.toList(growable: false),
currentEditingValue: currentTextEditingValue,
),
);
}
// null if no promptRect should be shown.
TextRange _currentPromptRectRange;
......
......@@ -22,6 +22,7 @@ export 'src/widgets/animated_switcher.dart';
export 'src/widgets/annotated_region.dart';
export 'src/widgets/app.dart';
export 'src/widgets/async.dart';
export 'src/widgets/autofill.dart';
export 'src/widgets/automatic_keep_alive.dart';
export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart';
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert' show utf8;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('TextInput message channels', () {
FakeTextChannel fakeTextChannel;
FakeAutofillScope scope;
setUp(() {
fakeTextChannel = FakeTextChannel((MethodCall call) async {});
TextInput.setChannel(fakeTextChannel);
scope ??= FakeAutofillScope();
scope.clients.clear();
});
tearDown(() {
TextInputConnection.debugResetId();
TextInput.setChannel(SystemChannels.textInput);
});
test('mandatory fields are mandatory', () async {
AutofillConfiguration config;
try {
config = AutofillConfiguration(
uniqueIdentifier: null,
autofillHints: const <String>['test'],
);
} catch (e) {
expect(e.toString(), contains('uniqueIdentifier != null'));
}
expect(config, isNull);
try {
config = AutofillConfiguration(
uniqueIdentifier: 'id',
autofillHints: null,
);
} catch (e) {
expect(e.toString(), contains('autofillHints != null'));
}
expect(config, isNull);
});
test('throws if the hint list is empty', () async {
Map<String, dynamic> json;
try {
const AutofillConfiguration config = AutofillConfiguration(
uniqueIdentifier: 'id',
autofillHints: <String>[],
);
json = config.toJson();
} catch (e) {
expect(e.toString(), contains('isNotEmpty'));
}
expect(json, isNull);
});
test(
'AutofillClients send the correct configuration to the platform'
'and responds to updateEditingStateWithTag method correctly',
() async {
final FakeAutofillClient client1 = FakeAutofillClient(const TextEditingValue(text: 'test1'));
final FakeAutofillClient client2 = FakeAutofillClient(const TextEditingValue(text: 'test2'));
client1.textInputConfiguration = TextInputConfiguration(
autofillConfiguration: AutofillConfiguration(
uniqueIdentifier: client1.autofillId,
autofillHints: const <String>['client1'],
currentEditingValue: client1.currentTextEditingValue,
),
);
client2.textInputConfiguration = TextInputConfiguration(
autofillConfiguration: AutofillConfiguration(
uniqueIdentifier: client2.autofillId,
autofillHints: const <String>['client2'],
currentEditingValue: client2.currentTextEditingValue,
),
);
scope.register(client1);
scope.register(client2);
client1.currentAutofillScope = scope;
client2.currentAutofillScope = scope;
scope.attach(client1, client1.textInputConfiguration);
final Map<String, dynamic> expectedConfiguration = client1.textInputConfiguration.toJson();
expectedConfiguration['fields'] = <Map<String, dynamic>>[
client1.textInputConfiguration.toJson(),
client2.textInputConfiguration.toJson(),
];
fakeTextChannel.validateOutgoingMethodCalls(<MethodCall>[
MethodCall('TextInput.setClient', <dynamic>[1, expectedConfiguration]),
]);
const TextEditingValue text2 = TextEditingValue(text: 'Text 2');
fakeTextChannel.incoming(MethodCall(
'TextInputClient.updateEditingStateWithTag',
<dynamic>[0, <String, dynamic>{ client2.autofillId : text2.toJSON() }],
));
expect(client2.currentTextEditingValue, text2);
});
});
}
class FakeAutofillClient implements TextInputClient, AutofillClient {
FakeAutofillClient(this.currentTextEditingValue);
@override
String get autofillId => hashCode.toString();
@override
TextInputConfiguration textInputConfiguration;
@override
void updateEditingValue(TextEditingValue newEditingValue) {
currentTextEditingValue = newEditingValue;
latestMethodCall = 'updateEditingValue';
}
@override
AutofillScope currentAutofillScope;
String latestMethodCall = '';
@override
TextEditingValue currentTextEditingValue;
@override
void performAction(TextInputAction action) {
latestMethodCall = 'performAction';
}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
latestMethodCall = 'updateFloatingCursor';
}
@override
void connectionClosed() {
latestMethodCall = 'connectionClosed';
}
@override
void showAutocorrectionPromptRect(int start, int end) {
latestMethodCall = 'showAutocorrectionPromptRect';
}
}
class FakeAutofillScope with AutofillScopeMixin implements AutofillScope {
final Map<String, AutofillClient> clients = <String, AutofillClient>{};
@override
Iterable<AutofillClient> get autofillClients => clients.values;
@override
AutofillClient getAutofillClient(String autofillId) => clients[autofillId];
void register(AutofillClient client) {
clients.putIfAbsent(client.autofillId, () => client);
}
}
class FakeTextChannel implements MethodChannel {
FakeTextChannel(this.outgoing) : assert(outgoing != null);
Future<dynamic> Function(MethodCall) outgoing;
Future<void> Function(MethodCall) incoming;
List<MethodCall> outgoingCalls = <MethodCall>[];
@override
BinaryMessenger get binaryMessenger => throw UnimplementedError();
@override
MethodCodec get codec => const JSONMethodCodec();
@override
Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError();
@override
Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError();
@override
Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
final MethodCall call = MethodCall(method, arguments);
outgoingCalls.add(call);
return await outgoing(call) as T;
}
@override
String get name => 'flutter/textinput';
@override
void setMethodCallHandler(Future<void> Function(MethodCall call) handler) {
incoming = handler;
}
@override
void setMockMethodCallHandler(Future<void> Function(MethodCall call) handler) => throw UnimplementedError();
void validateOutgoingMethodCalls(List<MethodCall> calls) {
expect(outgoingCalls.length, calls.length);
bool hasError = false;
for (int i = 0; i < calls.length; i++) {
final ByteData outgoingData = codec.encodeMethodCall(outgoingCalls[i]);
final ByteData expectedData = codec.encodeMethodCall(calls[i]);
final String outgoingString = utf8.decode(outgoingData.buffer.asUint8List());
final String expectedString = utf8.decode(expectedData.buffer.asUint8List());
if (outgoingString != expectedString) {
print(
'Index $i did not match:\n'
' actual: ${outgoingCalls[i]}\n'
' expected: ${calls[i]}');
hasError = true;
}
}
if (hasError) {
fail('Calls did not match.');
}
}
}
......@@ -200,6 +200,9 @@ class FakeTextInputClient implements TextInputClient {
@override
TextEditingValue currentTextEditingValue;
@override
AutofillScope get currentAutofillScope => null;
@override
void performAction(TextInputAction action) {
latestMethodCall = 'performAction';
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('AutofillGroup has the right clients', (WidgetTester tester) async {
const Key outerKey = Key('outer');
const Key innerKey = Key('inner');
const TextField client1 = TextField(autofillHints: <String>['1']);
const TextField client2 = TextField(autofillHints: <String>['2']);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: AutofillGroup(
key: outerKey,
child: Column(children: <Widget>[
client1,
AutofillGroup(
key: innerKey,
child: Column(children: const <Widget>[client2, TextField()]),
),
]),
),
),
),
);
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)),
);
expect(outerState.autofillClients, <EditableTextState>[clientState1]);
expect(innerState.autofillClients, <EditableTextState>[clientState2]);
});
testWidgets('new clients can be added & removed to a scope', (WidgetTester tester) async {
const Key scopeKey = Key('scope');
final List<String> hints = <String>[];
const TextField client1 = TextField(autofillHints: <String>['1']);
final TextField client2 = TextField(autofillHints: hints);
StateSetter setState;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: AutofillGroup(
key: scopeKey,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return Column(children: <Widget>[client1, client2]);
},
),
),
),
),
);
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)),
);
expect(scopeState.autofillClients, <EditableTextState>[clientState1]);
// Add to scope.
setState(() { hints.add('2'); });
await tester.pump();
expect(scopeState.autofillClients.length, 2);
expect(scopeState.autofillClients, contains(clientState1));
expect(scopeState.autofillClients, contains(clientState2));
// Remove from scope again.
setState(() { hints.clear(); });
await tester.pump();
expect(scopeState.autofillClients, <EditableTextState>[clientState1]);
});
testWidgets('AutofillGroup has the right clients after reparenting', (WidgetTester tester) async {
const Key outerKey = Key('outer');
const Key innerKey = Key('inner');
final GlobalKey keyClient3 = GlobalKey();
const TextField client1 = TextField(autofillHints: <String>['1']);
const TextField client2 = TextField(autofillHints: <String>['2']);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: AutofillGroup(
key: outerKey,
child: Column(children: <Widget>[
client1,
AutofillGroup(
key: innerKey,
child: Column(children: <Widget>[
client2,
TextField(key: keyClient3, autofillHints: const <String>['3']),
]),
),
]),
),
),
),
);
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)),
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: AutofillGroup(
key: outerKey,
child: Column(children: <Widget>[
client1,
TextField(key: keyClient3, autofillHints: const <String>['3']),
AutofillGroup(
key: innerKey,
child: Column(children: const <Widget>[client2]),
),
]),
),
),
),
);
expect(outerState.autofillClients.length, 2);
expect(outerState.autofillClients, contains(clientState1));
expect(outerState.autofillClients, contains(clientState3));
expect(innerState.autofillClients, <EditableTextState>[clientState2]);
});
}
......@@ -4116,8 +4116,17 @@ void main() {
await tester.showKeyboard(find.byType(EditableText));
// 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);
final List<String> logOrder = <String>[
'TextInput.setClient',
'TextInput.show',
'TextInput.setEditableSizeAndTransform',
'TextInput.requestAutofill',
'TextInput.setStyle',
'TextInput.setEditingState',
'TextInput.setEditingState',
'TextInput.show',
];
expect(tester.testTextInput.log.length, 8);
int index = 0;
for (final MethodCall m in tester.testTextInput.log) {
expect(m.method, logOrder[index]);
......@@ -4156,6 +4165,7 @@ void main() {
'TextInput.setClient',
'TextInput.show',
'TextInput.setEditableSizeAndTransform',
'TextInput.requestAutofill',
'TextInput.setStyle',
'TextInput.setEditingState',
'TextInput.setEditingState',
......@@ -4203,6 +4213,7 @@ void main() {
'TextInput.setClient',
'TextInput.show',
'TextInput.setEditableSizeAndTransform',
'TextInput.requestAutofill',
'TextInput.setStyle',
'TextInput.setEditingState',
'TextInput.setEditingState',
......
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