未验证 提交 d6aa099d 编写于 作者: N Nurhan Turgut 提交者: GitHub

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
上级 ad7be6e7
......@@ -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)
};
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册