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)
+    };