mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Autofill main part (flutter/engine#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
This commit is contained in:
parent
a3c4494c05
commit
fdeef18465
@ -2,7 +2,7 @@ chrome:
|
||||
# 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'
|
||||
|
||||
@ -74,7 +74,13 @@ class Keyboard {
|
||||
/// 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 @@ class Keyboard {
|
||||
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 @@ class Keyboard {
|
||||
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,
|
||||
};
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ class BrowserAutofillHints {
|
||||
'creditCardSecurityCode': 'cc-csc',
|
||||
'creditCardType': 'cc-type',
|
||||
'email': 'email',
|
||||
'familyName': 'familyName',
|
||||
'familyName': 'family-name',
|
||||
'fullStreetAddress': 'street-address',
|
||||
'gender': 'sex',
|
||||
'givenName': 'given-name',
|
||||
|
||||
@ -26,7 +26,6 @@ abstract class EngineInputType {
|
||||
return url;
|
||||
case 'TextInputType.multiline':
|
||||
return multiline;
|
||||
|
||||
case 'TextInputType.text':
|
||||
default:
|
||||
return text;
|
||||
|
||||
@ -49,6 +49,221 @@ void _setStaticStyleAttributes(html.HtmlElement domElement) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 @@ class EditingState {
|
||||
/// 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 @@ class InputConfiguration {
|
||||
@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 @@ class InputConfiguration {
|
||||
/// 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 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
}) {
|
||||
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 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
|
||||
@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 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
_subscriptions.clear();
|
||||
domElement.remove();
|
||||
domElement = null;
|
||||
_inputConfiguration.autofillGroup?.removeForm();
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
@ -458,11 +699,13 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
|
||||
@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 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
_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 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
|
||||
@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 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
|
||||
|
||||
@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 @@ class TextEditingChannel {
|
||||
|
||||
/// 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 @@ class TextEditingChannel {
|
||||
break;
|
||||
|
||||
case 'TextInput.setEditingState':
|
||||
implementation.setEditingState(EditingState.fromFrameworkMessage(call.arguments));
|
||||
implementation
|
||||
.setEditingState(EditingState.fromFrameworkMessage(call.arguments));
|
||||
break;
|
||||
|
||||
case 'TextInput.show':
|
||||
@ -801,11 +1059,13 @@ class TextEditingChannel {
|
||||
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 @@ class TextEditingChannel {
|
||||
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 @@ class HybridTextEditing {
|
||||
|
||||
/// 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 @@ class HybridTextEditing {
|
||||
},
|
||||
onAction: (String inputAction) {
|
||||
channel.performAction(_clientId, inputAction);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -1051,7 +1311,8 @@ class EditableTextStyle {
|
||||
@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'));
|
||||
|
||||
@ -787,6 +787,85 @@ void main() {
|
||||
expect(spy.messages, isEmpty);
|
||||
});
|
||||
|
||||
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',
|
||||
() {
|
||||
@ -1040,6 +1119,74 @@ void main() {
|
||||
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 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
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 @@ void checkTextAreaEditingState(
|
||||
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 @@ Map<String, dynamic> createFlutterConfig(
|
||||
'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)
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user