Autofill main part (#17986)
* changes for getting the configuration
* running autofill
* simplifications, remove unused map
* more changes
* make single autofill fields work. remove print messages
* remove an extra line
* remove extra file. also update chrome version
* addressing reviewers comments
* addressing reviewer comments
* addressing reviewer comments
* addressing reviewer comments
* changing comments
* changing comments
* adding a comment on subscriptions lifecycle
* fixing a bug which was failing the existing unit tests
* add unit tests for AutofillInfo and EngineAutofillForm. add autocomplete to textarea
* add unit tests for method channels
* remove json from the end of the file
* do not change the input type for the focused element
* check name instead of autocomplete for firefox
* check name instead of autocomplete for firefox in other methods as well
* fixing a bug in the autofillhints file, testing if firefox is failing for username hint or for all autocomplete values
* fix the breaking unit test
diff --git a/lib/web_ui/dev/browser_lock.yaml b/lib/web_ui/dev/browser_lock.yaml
index 2bede5b..535847d 100644
--- a/lib/web_ui/dev/browser_lock.yaml
+++ b/lib/web_ui/dev/browser_lock.yaml
@@ -2,7 +2,7 @@
# It seems Chrome can't always release from the same build for all operating
# systems, so we specify per-OS build number.
Linux: 753189
- Mac: 735116
+ Mac: 735194
Win: 735105
firefox:
version: '72.0'
diff --git a/lib/web_ui/lib/src/engine/keyboard.dart b/lib/web_ui/lib/src/engine/keyboard.dart
index 572d118..2569a05 100644
--- a/lib/web_ui/lib/src/engine/keyboard.dart
+++ b/lib/web_ui/lib/src/engine/keyboard.dart
@@ -74,7 +74,13 @@
/// Initializing with `0x0` which means no meta keys are pressed.
int _lastMetaState = 0x0;
- void _handleHtmlEvent(html.KeyboardEvent event) {
+ void _handleHtmlEvent(html.Event event) {
+ if (event is! html.KeyboardEvent) {
+ return;
+ }
+
+ final html.KeyboardEvent keyboardEvent = event as html.KeyboardEvent;
+
if (window._onPlatformMessage == null) {
return;
}
@@ -83,7 +89,7 @@
event.preventDefault();
}
- final String timerKey = event.code;
+ final String timerKey = keyboardEvent.code;
// Don't synthesize a keyup event for modifier keys because the browser always
// sends a keyup event for those.
@@ -111,8 +117,8 @@
final Map<String, dynamic> eventData = <String, dynamic>{
'type': event.type,
'keymap': 'web',
- 'code': event.code,
- 'key': event.key,
+ 'code': keyboardEvent.code,
+ 'key': keyboardEvent.key,
'metaState': _lastMetaState,
};
diff --git a/lib/web_ui/lib/src/engine/text_editing/autofill_hint.dart b/lib/web_ui/lib/src/engine/text_editing/autofill_hint.dart
index bd5f865..2b28aee 100644
--- a/lib/web_ui/lib/src/engine/text_editing/autofill_hint.dart
+++ b/lib/web_ui/lib/src/engine/text_editing/autofill_hint.dart
@@ -37,7 +37,7 @@
'creditCardSecurityCode': 'cc-csc',
'creditCardType': 'cc-type',
'email': 'email',
- 'familyName': 'familyName',
+ 'familyName': 'family-name',
'fullStreetAddress': 'street-address',
'gender': 'sex',
'givenName': 'given-name',
diff --git a/lib/web_ui/lib/src/engine/text_editing/input_type.dart b/lib/web_ui/lib/src/engine/text_editing/input_type.dart
index 9a82bfc..a3b46ee 100644
--- a/lib/web_ui/lib/src/engine/text_editing/input_type.dart
+++ b/lib/web_ui/lib/src/engine/text_editing/input_type.dart
@@ -26,7 +26,6 @@
return url;
case 'TextInputType.multiline':
return multiline;
-
case 'TextInputType.text':
default:
return text;
diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
index 64ed23f..5fcee58 100644
--- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
+++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
@@ -49,6 +49,221 @@
}
}
+/// Sets attributes to hide autofill elements.
+///
+/// These style attributes are constant throughout the life time of an input
+/// element.
+///
+/// They are assigned once during the creation of the DOM element.
+void _hideAutofillElements(html.HtmlElement domElement) {
+ final html.CssStyleDeclaration elementStyle = domElement.style;
+ elementStyle
+ ..whiteSpace = 'pre-wrap'
+ ..alignContent = 'center'
+ ..padding = '0'
+ ..opacity = '1'
+ ..color = 'transparent'
+ ..backgroundColor = 'transparent'
+ ..background = 'transparent'
+ ..outline = 'none'
+ ..border = 'none'
+ ..resize = 'none'
+ ..textShadow = 'transparent'
+ ..transformOrigin = '0 0 0';
+
+ /// This property makes the input's blinking cursor transparent.
+ elementStyle.setProperty('caret-color', 'transparent');
+}
+
+/// Form that contains all the fields in the same AutofillGroup.
+///
+/// These values are to be used when autofill is enabled and there is a group of
+/// text fields with more than one text field.
+class EngineAutofillForm {
+ EngineAutofillForm({this.formElement, this.elements, this.items});
+
+ final html.FormElement formElement;
+
+ final Map<String, html.HtmlElement> elements;
+
+ final Map<String, AutofillInfo> items;
+
+ factory EngineAutofillForm.fromFrameworkMessage(
+ Map<String, dynamic> focusedElementAutofill,
+ List<dynamic> fields,
+ ) {
+ // Autofill value can be null if focused text element does not have an
+ // autofill hint set.
+ if (focusedElementAutofill == null) {
+ return null;
+ }
+
+ // If there is only one text field in the autofill model, `fields` will be
+ // null. `focusedElementAutofill` contains the information about the one
+ // text field.
+ final bool singleElement = (fields == null);
+ final AutofillInfo focusedElement =
+ AutofillInfo.fromFrameworkMessage(focusedElementAutofill);
+ final Map<String, html.HtmlElement> elements = <String, html.HtmlElement>{};
+ final Map<String, AutofillInfo> items = <String, AutofillInfo>{};
+ final html.FormElement formElement = html.FormElement();
+
+ // Validation is in the framework side.
+ formElement.noValidate = true;
+
+ _hideAutofillElements(formElement);
+
+ if (!singleElement) {
+ for (Map<String, dynamic> field in fields) {
+ final Map<String, dynamic> autofillInfo = field['autofill'];
+ final AutofillInfo autofill =
+ AutofillInfo.fromFrameworkMessage(autofillInfo);
+
+ // The focused text editing element will not be created here.
+ if (autofill.uniqueIdentifier != focusedElement.uniqueIdentifier) {
+ EngineInputType engineInputType =
+ EngineInputType.fromName(field['inputType']['name']);
+
+ html.HtmlElement htmlElement = engineInputType.createDomElement();
+ autofill.editingState.applyToDomElement(htmlElement);
+ autofill.applyToDomElement(htmlElement);
+ _hideAutofillElements(htmlElement);
+
+ items[autofill.uniqueIdentifier] = autofill;
+ elements[autofill.uniqueIdentifier] = htmlElement;
+ formElement.append(htmlElement);
+ }
+ }
+ }
+
+ return EngineAutofillForm(
+ formElement: formElement,
+ elements: elements,
+ items: items,
+ );
+ }
+
+ void placeForm(html.HtmlElement mainTextEditingElement) {
+ formElement.append(mainTextEditingElement);
+ domRenderer.glassPaneElement.append(formElement);
+ }
+
+ void removeForm() {
+ formElement.remove();
+ }
+
+ /// Listens to `onInput` event on the form fields.
+ ///
+ /// Registering to the listeners could have been done in the constructor.
+ /// On the other hand, overall for text editing there is already a lifecycle
+ /// for subscriptions: All the subscriptions of the DOM elements are to the
+ /// `_subscriptions` property of [DefaultTextEditingStrategy].
+ /// [TextEditingStrategy] manages all subscription lifecyle. All
+ /// listeners with no exceptions are added during
+ /// [TextEditingStrategy.addEventHandlers] method call and all
+ /// listeners are removed during [TextEditingStrategy.disable] method call.
+ List<StreamSubscription<html.Event>> addInputEventListeners() {
+ Iterable<String> keys = elements.keys;
+ List<StreamSubscription<html.Event>> subscriptions =
+ <StreamSubscription<html.Event>>[];
+ keys.forEach((String key) {
+ final html.Element element = elements[key];
+ subscriptions.add(element.onInput.listen((html.Event e) {
+ _handleChange(element, key);
+ }));
+ });
+ return subscriptions;
+ }
+
+ void _handleChange(html.Element domElement, String tag) {
+ EditingState newEditingState = EditingState.fromDomElement(domElement);
+
+ _sendAutofillEditingState(tag, newEditingState);
+ }
+
+ /// Sends the 'TextInputClient.updateEditingStateWithTag' message to the framework.
+ void _sendAutofillEditingState(String tag, EditingState editingState) {
+ if (window._onPlatformMessage != null) {
+ window.invokeOnPlatformMessage(
+ 'flutter/textinput',
+ const JSONMethodCodec().encodeMethodCall(
+ MethodCall(
+ 'TextInputClient.updateEditingStateWithTag',
+ <dynamic>[
+ 0,
+ <String, dynamic>{tag: editingState.toFlutter()}
+ ],
+ ),
+ ),
+ _emptyCallback,
+ );
+ }
+ }
+}
+
+/// Autofill related values.
+///
+/// These values are to be used when a text field have autofill enabled.
+@visibleForTesting
+class AutofillInfo {
+ AutofillInfo({this.editingState, this.uniqueIdentifier, this.hint});
+
+ /// The current text and selection state of a text field.
+ final EditingState editingState;
+
+ /// Unique value set by the developer.
+ ///
+ /// Used as id of the text field.
+ final String uniqueIdentifier;
+
+ /// Attribute used for autofill.
+ ///
+ /// Used as a guidance to the browser as to the type of information expected
+ /// in the field.
+ /// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
+ final String hint;
+
+ factory AutofillInfo.fromFrameworkMessage(Map<String, dynamic> autofill) {
+ // Autofill value can be null if no TextFields is set with autofill hint.
+ if (autofill == null) {
+ return null;
+ }
+
+ final String uniqueIdentifier = autofill['uniqueIdentifier'];
+ final List<dynamic> hintsList = autofill['hints'];
+ final EditingState editingState =
+ EditingState.fromFrameworkMessage(autofill['editingValue']);
+ return AutofillInfo(
+ uniqueIdentifier: uniqueIdentifier,
+ hint: BrowserAutofillHints.instance.flutterToEngine(hintsList[0]),
+ editingState: editingState);
+ }
+
+ void applyToDomElement(html.HtmlElement domElement,
+ {bool focusedElement = false}) {
+ domElement.id = hint;
+ if (domElement is html.InputElement) {
+ html.InputElement element = domElement;
+ element.name = hint;
+ element.id = uniqueIdentifier;
+ element.autocomplete = hint;
+ // Do not change the element type for the focused element.
+ if (focusedElement == false) {
+ if (hint.contains('password')) {
+ element.type = 'password';
+ } else {
+ element.type = 'text';
+ }
+ }
+ } else if (domElement is html.TextAreaElement) {
+ html.TextAreaElement element = domElement;
+ element.name = hint;
+ element.id = uniqueIdentifier;
+ element.setAttribute('autocomplete', hint);
+ }
+ }
+}
+
/// The current text and selection state of a text field.
@visibleForTesting
class EditingState {
@@ -73,7 +288,8 @@
/// Flutter Framework can send the [selectionBase] and [selectionExtent] as
/// -1, if so 0 assigned to the [baseOffset] and [extentOffset]. -1 is not a
/// valid selection range for input DOM elements.
- factory EditingState.fromFrameworkMessage(Map<String, dynamic> flutterEditingState) {
+ factory EditingState.fromFrameworkMessage(
+ Map<String, dynamic> flutterEditingState) {
final int selectionBase = flutterEditingState['selectionBase'];
final int selectionExtent = flutterEditingState['selectionExtent'];
final String text = flutterEditingState['text'];
@@ -183,14 +399,21 @@
@required this.inputAction,
@required this.obscureText,
@required this.autocorrect,
+ this.autofill,
+ this.autofillGroup,
});
-
- InputConfiguration.fromFrameworkMessage(Map<String, dynamic> flutterInputConfiguration)
+ InputConfiguration.fromFrameworkMessage(
+ Map<String, dynamic> flutterInputConfiguration)
: inputType = EngineInputType.fromName(
flutterInputConfiguration['inputType']['name']),
inputAction = flutterInputConfiguration['inputAction'],
obscureText = flutterInputConfiguration['obscureText'],
- autocorrect = flutterInputConfiguration['autocorrect'];
+ autocorrect = flutterInputConfiguration['autocorrect'],
+ autofill = AutofillInfo.fromFrameworkMessage(
+ flutterInputConfiguration['autofill']),
+ autofillGroup = EngineAutofillForm.fromFrameworkMessage(
+ flutterInputConfiguration['autofill'],
+ flutterInputConfiguration['fields']);
/// The type of information being edited in the input control.
final EngineInputType inputType;
@@ -209,6 +432,10 @@
/// For future manual tests, note that autocorrect is an attribute only
/// supported by Safari.
final bool autocorrect;
+
+ final AutofillInfo autofill;
+
+ final EngineAutofillForm autofillGroup;
}
typedef _OnChangeCallback = void Function(EditingState editingState);
@@ -330,21 +557,29 @@
}) {
assert(!isEnabled);
+ this._inputConfiguration = inputConfig;
+
domElement = inputConfig.inputType.createDomElement();
if (inputConfig.obscureText) {
domElement.setAttribute('type', 'password');
}
+ inputConfig.autofill?.applyToDomElement(domElement, focusedElement: true);
+
final String autocorrectValue = inputConfig.autocorrect ? 'on' : 'off';
domElement.setAttribute('autocorrect', autocorrectValue);
_setStaticStyleAttributes(domElement);
_style?.applyToDomElement(domElement);
+ if (_inputConfiguration.autofillGroup != null) {
+ _inputConfiguration.autofillGroup.placeForm(domElement);
+ } else {
+ domRenderer.glassPaneElement.append(domElement);
+ }
+
initializeElementPlacement();
- domRenderer.glassPaneElement.append(domElement);
isEnabled = true;
- _inputConfiguration = inputConfig;
_onChange = onChange;
_onAction = onAction;
}
@@ -356,6 +591,11 @@
@override
void addEventHandlers() {
+ if (_inputConfiguration.autofillGroup != null) {
+ _subscriptions
+ .addAll(_inputConfiguration.autofillGroup.addInputEventListeners());
+ }
+
// Subscribe to text and selection changes.
_subscriptions.add(domElement.onInput.listen(_handleChange));
@@ -425,6 +665,7 @@
_subscriptions.clear();
domElement.remove();
domElement = null;
+ _inputConfiguration.autofillGroup?.removeForm();
}
@mustCallSuper
@@ -458,11 +699,13 @@
}
}
- void _maybeSendAction(html.KeyboardEvent event) {
- if (_inputConfiguration.inputType.submitActionOnEnter &&
- event.keyCode == _kReturnKeyCode) {
- event.preventDefault();
- _onAction(_inputConfiguration.inputAction);
+ void _maybeSendAction(html.Event event) {
+ if (event is html.KeyboardEvent) {
+ if (_inputConfiguration.inputType.submitActionOnEnter &&
+ event.keyCode == _kReturnKeyCode) {
+ event.preventDefault();
+ _onAction(_inputConfiguration.inputAction);
+ }
}
}
@@ -581,6 +824,11 @@
@override
void addEventHandlers() {
+ if (_inputConfiguration.autofillGroup != null) {
+ _subscriptions
+ .addAll(_inputConfiguration.autofillGroup.addInputEventListeners());
+ }
+
// Subscribe to text and selection changes.
_subscriptions.add(domElement.onInput.listen(_handleChange));
@@ -594,7 +842,7 @@
_schedulePlacement();
}));
- _addTapListener();
+ _addTapListener();
// On iOS, blur is trigerred if the virtual keyboard is closed or the
// browser is sent to background or the browser tab is changed.
@@ -685,6 +933,11 @@
@override
void addEventHandlers() {
+ if (_inputConfiguration.autofillGroup != null) {
+ _subscriptions
+ .addAll(_inputConfiguration.autofillGroup.addInputEventListeners());
+ }
+
// Subscribe to text and selection changes.
_subscriptions.add(domElement.onInput.listen(_handleChange));
@@ -715,6 +968,11 @@
@override
void addEventHandlers() {
+ if (_inputConfiguration.autofillGroup != null) {
+ _subscriptions
+ .addAll(_inputConfiguration.autofillGroup.addInputEventListeners());
+ }
+
// Subscribe to text and selection changes.
_subscriptions.add(domElement.onInput.listen(_handleChange));
@@ -780,8 +1038,7 @@
/// Handles "flutter/textinput" platform messages received from the framework.
void handleTextInput(
- ByteData data,
- ui.PlatformMessageResponseCallback callback) {
+ ByteData data, ui.PlatformMessageResponseCallback callback) {
const JSONMethodCodec codec = JSONMethodCodec();
final MethodCall call = codec.decodeMethodCall(data);
switch (call.method) {
@@ -793,7 +1050,8 @@
break;
case 'TextInput.setEditingState':
- implementation.setEditingState(EditingState.fromFrameworkMessage(call.arguments));
+ implementation
+ .setEditingState(EditingState.fromFrameworkMessage(call.arguments));
break;
case 'TextInput.show':
@@ -801,11 +1059,13 @@
break;
case 'TextInput.setEditableSizeAndTransform':
- implementation.setEditableSizeAndTransform(EditableTextGeometry.fromFrameworkMessage(call.arguments));
+ implementation.setEditableSizeAndTransform(
+ EditableTextGeometry.fromFrameworkMessage(call.arguments));
break;
case 'TextInput.setStyle':
- implementation.setStyle(EditableTextStyle.fromFrameworkMessage(call.arguments));
+ implementation
+ .setStyle(EditableTextStyle.fromFrameworkMessage(call.arguments));
break;
case 'TextInput.clearClient':
@@ -822,7 +1082,8 @@
break;
default:
- throw StateError('Unsupported method call on the flutter/textinput channel: ${call.method}');
+ throw StateError(
+ 'Unsupported method call on the flutter/textinput channel: ${call.method}');
}
window._replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
}
@@ -941,8 +1202,7 @@
/// Responds to the 'TextInput.setEditingState' message.
void setEditingState(EditingState state) {
- editingElement
- .setEditingState(state);
+ editingElement.setEditingState(state);
}
/// Responds to the 'TextInput.show' message.
@@ -1021,7 +1281,7 @@
},
onAction: (String inputAction) {
channel.performAction(_clientId, inputAction);
- }
+ },
);
}
@@ -1051,7 +1311,8 @@
@required this.fontWeight,
});
- factory EditableTextStyle.fromFrameworkMessage(Map<String, dynamic> flutterStyle) {
+ factory EditableTextStyle.fromFrameworkMessage(
+ Map<String, dynamic> flutterStyle) {
assert(flutterStyle.containsKey('fontSize'));
assert(flutterStyle.containsKey('fontFamily'));
assert(flutterStyle.containsKey('textAlignIndex'));
diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart
index 6b64aae..dbc760c 100644
--- a/lib/web_ui/test/text_editing_test.dart
+++ b/lib/web_ui/test/text_editing_test.dart
@@ -788,6 +788,85 @@
});
test(
+ 'singleTextField Autofill: setClient, setEditingState, show, '
+ 'setEditingState, clearClient', () {
+ // Create a configuration with focused element has autofil hint.
+ final Map<String, dynamic> flutterSingleAutofillElementConfig =
+ createFlutterConfig('text', autofillHint: 'username');
+ final MethodCall setClient = MethodCall('TextInput.setClient',
+ <dynamic>[123, flutterSingleAutofillElementConfig]);
+ sendFrameworkMessage(codec.encodeMethodCall(setClient));
+
+ const MethodCall setEditingState1 =
+ MethodCall('TextInput.setEditingState', <String, dynamic>{
+ 'text': 'abcd',
+ 'selectionBase': 2,
+ 'selectionExtent': 3,
+ });
+ sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
+
+ const MethodCall show = MethodCall('TextInput.show');
+ sendFrameworkMessage(codec.encodeMethodCall(show));
+
+ // The second [setEditingState] should override the first one.
+ checkInputEditingState(
+ textEditing.editingElement.domElement, 'abcd', 2, 3);
+
+ final FormElement formElement = document.getElementsByTagName('form')[0];
+ expect(formElement.childNodes, hasLength(1));
+
+ const MethodCall clearClient = MethodCall('TextInput.clearClient');
+ sendFrameworkMessage(codec.encodeMethodCall(clearClient));
+
+ // Confirm that [HybridTextEditing] didn't send any messages.
+ expect(spy.messages, isEmpty);
+ expect(document.getElementsByTagName('form'), isEmpty);
+ });
+
+ test(
+ 'multiTextField Autofill: setClient, setEditingState, show, '
+ 'setEditingState, clearClient', () {
+ // Create a configuration with an AutofillGroup of four text fields.
+ final Map<String, dynamic> flutterMultiAutofillElementConfig =
+ createFlutterConfig('text',
+ autofillHint: 'username',
+ autofillHintsForFields: [
+ 'username',
+ 'email',
+ 'name',
+ 'telephoneNumber'
+ ]);
+ final MethodCall setClient = MethodCall('TextInput.setClient',
+ <dynamic>[123, flutterMultiAutofillElementConfig]);
+ sendFrameworkMessage(codec.encodeMethodCall(setClient));
+
+ const MethodCall setEditingState1 =
+ MethodCall('TextInput.setEditingState', <String, dynamic>{
+ 'text': 'abcd',
+ 'selectionBase': 2,
+ 'selectionExtent': 3,
+ });
+ sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
+
+ const MethodCall show = MethodCall('TextInput.show');
+ sendFrameworkMessage(codec.encodeMethodCall(show));
+
+ // The second [setEditingState] should override the first one.
+ checkInputEditingState(
+ textEditing.editingElement.domElement, 'abcd', 2, 3);
+
+ final FormElement formElement = document.getElementsByTagName('form')[0];
+ expect(formElement.childNodes, hasLength(4));
+
+ const MethodCall clearClient = MethodCall('TextInput.clearClient');
+ sendFrameworkMessage(codec.encodeMethodCall(clearClient));
+
+ // Confirm that [HybridTextEditing] didn't send any messages.
+ expect(spy.messages, isEmpty);
+ expect(document.getElementsByTagName('form'), isEmpty);
+ });
+
+ test(
'setClient, setEditableSizeAndTransform, setStyle, setEditingState, show, clearClient',
() {
final MethodCall setClient = MethodCall(
@@ -1040,6 +1119,74 @@
hideKeyboard();
});
+ test('multiTextField Autofill sync updates back to Flutter', () {
+ // Create a configuration with an AutofillGroup of four text fields.
+ final String hintForFirstElement = 'familyName';
+ final Map<String, dynamic> flutterMultiAutofillElementConfig =
+ createFlutterConfig('text',
+ autofillHint: 'email',
+ autofillHintsForFields: [
+ hintForFirstElement,
+ 'email',
+ 'givenName',
+ 'telephoneNumber'
+ ]);
+ final MethodCall setClient = MethodCall('TextInput.setClient',
+ <dynamic>[123, flutterMultiAutofillElementConfig]);
+ sendFrameworkMessage(codec.encodeMethodCall(setClient));
+
+ const MethodCall setEditingState1 =
+ MethodCall('TextInput.setEditingState', <String, dynamic>{
+ 'text': 'abcd',
+ 'selectionBase': 2,
+ 'selectionExtent': 3,
+ });
+ sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
+
+ const MethodCall show = MethodCall('TextInput.show');
+ sendFrameworkMessage(codec.encodeMethodCall(show));
+
+ // The second [setEditingState] should override the first one.
+ checkInputEditingState(
+ textEditing.editingElement.domElement, 'abcd', 2, 3);
+
+ final FormElement formElement = document.getElementsByTagName('form')[0];
+ expect(formElement.childNodes, hasLength(4));
+
+ // Autofill one of the form elements.
+ InputElement element = formElement.childNodes.first;
+ if (browserEngine == BrowserEngine.firefox) {
+ expect(element.name,
+ BrowserAutofillHints.instance.flutterToEngine(hintForFirstElement));
+ } else {
+ expect(element.autocomplete,
+ BrowserAutofillHints.instance.flutterToEngine(hintForFirstElement));
+ }
+ element.value = 'something';
+ element.dispatchEvent(Event.eventType('Event', 'input'));
+
+ expect(spy.messages, hasLength(1));
+ expect(spy.messages[0].channel, 'flutter/textinput');
+ expect(spy.messages[0].methodName,
+ 'TextInputClient.updateEditingStateWithTag');
+ expect(
+ spy.messages[0].methodArguments,
+ <dynamic>[
+ 0, // Client ID
+ <String, dynamic>{
+ hintForFirstElement: <String, dynamic>{
+ 'text': 'something',
+ 'selectionBase': 9,
+ 'selectionExtent': 9
+ }
+ },
+ ],
+ );
+
+ spy.messages.clear();
+ hideKeyboard();
+ });
+
test('Multi-line mode also works', () {
final MethodCall setClient = MethodCall(
'TextInput.setClient', <dynamic>[123, flutterMultilineConfig]);
@@ -1215,6 +1362,169 @@
});
});
+ group('EngineAutofillForm', () {
+ test('validate multi element form', () {
+ final List<dynamic> fields = createFieldValues(
+ ['username', 'password', 'newPassword'],
+ ['field1', 'fields2', 'field3']);
+ final EngineAutofillForm autofillForm =
+ EngineAutofillForm.fromFrameworkMessage(
+ createAutofillInfo('username', 'field1'), fields);
+
+ // Number of elements if number of fields sent to the constructor minus
+ // one (for the focused text element).
+ expect(autofillForm.elements, hasLength(2));
+ expect(autofillForm.items, hasLength(2));
+ expect(autofillForm.formElement, isNotNull);
+
+ final FormElement form = autofillForm.formElement;
+ expect(form.childNodes, hasLength(2));
+
+ final InputElement firstElement = form.childNodes.first;
+ // Autofill value is applied to the element.
+ expect(firstElement.name,
+ BrowserAutofillHints.instance.flutterToEngine('password'));
+ expect(firstElement.id, 'fields2');
+ expect(firstElement.type, 'password');
+ if (browserEngine == BrowserEngine.firefox) {
+ expect(firstElement.name,
+ BrowserAutofillHints.instance.flutterToEngine('password'));
+ } else {
+ expect(firstElement.autocomplete,
+ BrowserAutofillHints.instance.flutterToEngine('password'));
+ }
+
+ // Editing state is applied to the element.
+ expect(firstElement.value, 'Test');
+ expect(firstElement.selectionStart, 0);
+ expect(firstElement.selectionEnd, 0);
+
+ // Element is hidden.
+ final CssStyleDeclaration css = firstElement.style;
+ expect(css.color, 'transparent');
+ expect(css.backgroundColor, 'transparent');
+ });
+
+ test('place remove form', () {
+ final List<dynamic> fields = createFieldValues(
+ ['username', 'password', 'newPassword'],
+ ['field1', 'fields2', 'field3']);
+ final EngineAutofillForm autofillForm =
+ EngineAutofillForm.fromFrameworkMessage(
+ createAutofillInfo('username', 'field1'), fields);
+
+ final InputElement testInputElement = InputElement();
+ autofillForm.placeForm(testInputElement);
+
+ // The focused element is appended to the form,
+ final FormElement form = autofillForm.formElement;
+ expect(form.childNodes, hasLength(3));
+
+ final FormElement formOnDom = document.getElementsByTagName('form')[0];
+ // Form is attached to the DOM.
+ expect(form, equals(formOnDom));
+
+ autofillForm.removeForm();
+ expect(document.getElementsByTagName('form'), isEmpty);
+ });
+
+ test('Validate single element form', () {
+ final List<dynamic> fields = createFieldValues(['username'], ['field1']);
+ final EngineAutofillForm autofillForm =
+ EngineAutofillForm.fromFrameworkMessage(
+ createAutofillInfo('username', 'field1'), fields);
+
+ // The focused element is the only field. Form should be empty after
+ // the initialization (focus element is appended later).
+ expect(autofillForm.elements, isEmpty);
+ expect(autofillForm.items, isEmpty);
+ expect(autofillForm.formElement, isNotNull);
+
+ final FormElement form = autofillForm.formElement;
+ expect(form.childNodes, isEmpty);
+ });
+
+ test('Return null if no focused element', () {
+ final List<dynamic> fields = createFieldValues(['username'], ['field1']);
+ final EngineAutofillForm autofillForm =
+ EngineAutofillForm.fromFrameworkMessage(null, fields);
+
+ expect(autofillForm, isNull);
+ });
+ });
+
+ group('AutofillInfo', () {
+ const String testHint = 'streetAddressLine2';
+ const String testId = 'EditableText-659836579';
+ const String testPasswordHint = 'password';
+
+ test('autofill has correct value', () {
+ final AutofillInfo autofillInfo = AutofillInfo.fromFrameworkMessage(
+ createAutofillInfo(testHint, testId));
+
+ // Hint sent from the framework is converted to the hint compatible with
+ // browsers.
+ expect(autofillInfo.hint,
+ BrowserAutofillHints.instance.flutterToEngine(testHint));
+ expect(autofillInfo.uniqueIdentifier, testId);
+ });
+
+ test('input with autofill hint', () {
+ final AutofillInfo autofillInfo = AutofillInfo.fromFrameworkMessage(
+ createAutofillInfo(testHint, testId));
+
+ final InputElement testInputElement = InputElement();
+ autofillInfo.applyToDomElement(testInputElement);
+
+ // Hint sent from the framework is converted to the hint compatible with
+ // browsers.
+ expect(testInputElement.name,
+ BrowserAutofillHints.instance.flutterToEngine(testHint));
+ expect(testInputElement.id, testId);
+ expect(testInputElement.type, 'text');
+ if (browserEngine == BrowserEngine.firefox) {
+ expect(testInputElement.name,
+ BrowserAutofillHints.instance.flutterToEngine(testHint));
+ } else {
+ expect(testInputElement.autocomplete,
+ BrowserAutofillHints.instance.flutterToEngine(testHint));
+ }
+ });
+
+ test('textarea with autofill hint', () {
+ final AutofillInfo autofillInfo = AutofillInfo.fromFrameworkMessage(
+ createAutofillInfo(testHint, testId));
+
+ final TextAreaElement testInputElement = TextAreaElement();
+ autofillInfo.applyToDomElement(testInputElement);
+
+ // Hint sent from the framework is converted to the hint compatible with
+ // browsers.
+ expect(testInputElement.name,
+ BrowserAutofillHints.instance.flutterToEngine(testHint));
+ expect(testInputElement.id, testId);
+ expect(testInputElement.getAttribute('autocomplete'),
+ BrowserAutofillHints.instance.flutterToEngine(testHint));
+ });
+
+ test('password autofill hint', () {
+ final AutofillInfo autofillInfo = AutofillInfo.fromFrameworkMessage(
+ createAutofillInfo(testPasswordHint, testId));
+
+ final InputElement testInputElement = InputElement();
+ autofillInfo.applyToDomElement(testInputElement);
+
+ // Hint sent from the framework is converted to the hint compatible with
+ // browsers.
+ expect(testInputElement.name,
+ BrowserAutofillHints.instance.flutterToEngine(testPasswordHint));
+ expect(testInputElement.id, testId);
+ expect(testInputElement.type, 'password');
+ expect(testInputElement.getAttribute('autocomplete'),
+ BrowserAutofillHints.instance.flutterToEngine(testPasswordHint));
+ });
+ });
+
group('EditingState', () {
EditingState _editingState;
@@ -1399,11 +1709,17 @@
expect(textarea.selectionEnd, end);
}
+/// Creates an [InputConfiguration] for using in the tests.
+///
+/// For simplicity this method is using `autofillHint` as the `uniqueId` for
+/// simplicity.
Map<String, dynamic> createFlutterConfig(
String inputType, {
bool obscureText = false,
bool autocorrect = true,
String inputAction,
+ String autofillHint,
+ List<String> autofillHintsForFields,
}) {
return <String, dynamic>{
'inputType': <String, String>{
@@ -1412,5 +1728,47 @@
'obscureText': obscureText,
'autocorrect': autocorrect,
'inputAction': inputAction ?? 'TextInputAction.done',
+ if (autofillHint != null)
+ 'autofill': createAutofillInfo(autofillHint, autofillHint),
+ if (autofillHintsForFields != null)
+ 'fields':
+ createFieldValues(autofillHintsForFields, autofillHintsForFields),
};
}
+
+Map<String, dynamic> createAutofillInfo(String hint, String uniqueId) =>
+ <String, dynamic>{
+ 'uniqueIdentifier': uniqueId,
+ 'hints': [hint],
+ 'editingValue': {
+ 'text': 'Test',
+ 'selectionBase': 0,
+ 'selectionExtent': 0,
+ 'selectionAffinity': 'TextAffinity.downstream',
+ 'selectionIsDirectional': false,
+ 'composingBase': -1,
+ 'composingExtent': -1,
+ },
+ };
+
+List<dynamic> createFieldValues(List<String> hints, List<String> uniqueIds) {
+ final List<dynamic> testFields = <dynamic>[];
+
+ expect(hints.length, equals(uniqueIds.length));
+
+ for (int i = 0; i < hints.length; i++) {
+ testFields.add(createOneFieldValue(hints[i], uniqueIds[i]));
+ }
+
+ return testFields;
+}
+
+Map<String, dynamic> createOneFieldValue(String hint, String uniqueId) =>
+ <String, dynamic>{
+ 'inputType': {
+ 'name': 'TextInputType.text',
+ 'signed': null,
+ 'decimal': null
+ },
+ 'autofill': createAutofillInfo(hint, uniqueId)
+ };