mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
2266 lines
80 KiB
Dart
2266 lines
80 KiB
Dart
// Copyright 2013 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// @dart = 2.6
|
|
import 'dart:async';
|
|
import 'dart:html';
|
|
import 'dart:js_util' as js_util;
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:test/bootstrap/browser.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import 'package:ui/src/engine.dart' hide window;
|
|
|
|
import 'matchers.dart';
|
|
import 'spy.dart';
|
|
|
|
/// The `keyCode` of the "Enter" key.
|
|
const int _kReturnKeyCode = 13;
|
|
|
|
const MethodCodec codec = JSONMethodCodec();
|
|
|
|
/// Add unit tests for [FirefoxTextEditingStrategy].
|
|
/// TODO(nurhan): https://github.com/flutter/flutter/issues/46891
|
|
|
|
DefaultTextEditingStrategy editingElement;
|
|
EditingState lastEditingState;
|
|
String lastInputAction;
|
|
|
|
final InputConfiguration singlelineConfig = InputConfiguration(
|
|
inputType: EngineInputType.text,
|
|
);
|
|
final Map<String, dynamic> flutterSinglelineConfig =
|
|
createFlutterConfig('text');
|
|
|
|
final InputConfiguration multilineConfig = InputConfiguration(
|
|
inputType: EngineInputType.multiline,
|
|
inputAction: 'TextInputAction.newline',
|
|
);
|
|
final Map<String, dynamic> flutterMultilineConfig =
|
|
createFlutterConfig('multiline');
|
|
|
|
void trackEditingState(EditingState editingState) {
|
|
lastEditingState = editingState;
|
|
}
|
|
|
|
void trackInputAction(String inputAction) {
|
|
lastInputAction = inputAction;
|
|
}
|
|
|
|
void main() {
|
|
internalBootstrapBrowserTest(() => testMain);
|
|
}
|
|
|
|
void testMain() {
|
|
tearDown(() {
|
|
lastEditingState = null;
|
|
lastInputAction = null;
|
|
cleanTextEditingElement();
|
|
cleanTestFlags();
|
|
clearBackUpDomElementIfExists();
|
|
});
|
|
|
|
group('$GloballyPositionedTextEditingStrategy', () {
|
|
HybridTextEditing testTextEditing;
|
|
|
|
setUp(() {
|
|
testTextEditing = HybridTextEditing();
|
|
editingElement = GloballyPositionedTextEditingStrategy(testTextEditing);
|
|
testTextEditing.useCustomEditableElement(editingElement);
|
|
});
|
|
|
|
test('Creates element when enabled and removes it when disabled', () {
|
|
expect(
|
|
document.getElementsByTagName('input'),
|
|
hasLength(0),
|
|
);
|
|
// The focus initially is on the body.
|
|
expect(document.activeElement, document.body);
|
|
|
|
editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(
|
|
document.getElementsByTagName('input'),
|
|
hasLength(1),
|
|
);
|
|
final InputElement input = document.getElementsByTagName('input')[0];
|
|
// Now the editing element should have focus.
|
|
expect(document.activeElement, input);
|
|
expect(editingElement.domElement, input);
|
|
expect(input.getAttribute('type'), null);
|
|
|
|
// Input is appended to the glass pane.
|
|
expect(domRenderer.glassPaneElement.contains(editingElement.domElement),
|
|
isTrue);
|
|
|
|
editingElement.disable();
|
|
expect(
|
|
document.getElementsByTagName('input'),
|
|
hasLength(0),
|
|
);
|
|
// The focus is back to the body.
|
|
expect(document.activeElement, document.body);
|
|
});
|
|
|
|
test('Respects read-only config', () {
|
|
final InputConfiguration config = InputConfiguration(readOnly: true);
|
|
editingElement.enable(
|
|
config,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(document.getElementsByTagName('input'), hasLength(1));
|
|
final InputElement input = document.getElementsByTagName('input')[0];
|
|
expect(editingElement.domElement, input);
|
|
expect(input.getAttribute('readonly'), 'readonly');
|
|
|
|
editingElement.disable();
|
|
});
|
|
|
|
test('Knows how to create password fields', () {
|
|
final InputConfiguration config = InputConfiguration(obscureText: true);
|
|
editingElement.enable(
|
|
config,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(document.getElementsByTagName('input'), hasLength(1));
|
|
final InputElement input = document.getElementsByTagName('input')[0];
|
|
expect(editingElement.domElement, input);
|
|
expect(input.getAttribute('type'), 'password');
|
|
|
|
editingElement.disable();
|
|
});
|
|
|
|
test('Knows to turn autocorrect off', () {
|
|
final InputConfiguration config = InputConfiguration(autocorrect: false);
|
|
editingElement.enable(
|
|
config,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(document.getElementsByTagName('input'), hasLength(1));
|
|
final InputElement input = document.getElementsByTagName('input')[0];
|
|
expect(editingElement.domElement, input);
|
|
expect(input.getAttribute('autocorrect'), 'off');
|
|
|
|
editingElement.disable();
|
|
});
|
|
|
|
test('Knows to turn autocorrect on', () {
|
|
final InputConfiguration config = InputConfiguration(autocorrect: true);
|
|
editingElement.enable(
|
|
config,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(document.getElementsByTagName('input'), hasLength(1));
|
|
final InputElement input = document.getElementsByTagName('input')[0];
|
|
expect(editingElement.domElement, input);
|
|
expect(input.getAttribute('autocorrect'), 'on');
|
|
|
|
editingElement.disable();
|
|
});
|
|
|
|
test('Can read editing state correctly', () {
|
|
editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
|
|
final InputElement input = editingElement.domElement;
|
|
input.value = 'foo bar';
|
|
input.dispatchEvent(Event.eventType('Event', 'input'));
|
|
expect(
|
|
lastEditingState,
|
|
EditingState(text: 'foo bar', baseOffset: 7, extentOffset: 7),
|
|
);
|
|
|
|
input.setSelectionRange(4, 6);
|
|
document.dispatchEvent(Event.eventType('Event', 'selectionchange'));
|
|
expect(
|
|
lastEditingState,
|
|
EditingState(text: 'foo bar', baseOffset: 4, extentOffset: 6),
|
|
);
|
|
|
|
// There should be no input action.
|
|
expect(lastInputAction, isNull);
|
|
});
|
|
|
|
test('Can set editing state correctly', () {
|
|
editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
editingElement.setEditingState(
|
|
EditingState(text: 'foo bar baz', baseOffset: 2, extentOffset: 7));
|
|
|
|
checkInputEditingState(editingElement.domElement, 'foo bar baz', 2, 7);
|
|
|
|
// There should be no input action.
|
|
expect(lastInputAction, isNull);
|
|
});
|
|
|
|
test('Multi-line mode also works', () {
|
|
// The textarea element is created lazily.
|
|
expect(document.getElementsByTagName('textarea'), hasLength(0));
|
|
editingElement.enable(
|
|
multilineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(document.getElementsByTagName('textarea'), hasLength(1));
|
|
|
|
final TextAreaElement textarea =
|
|
document.getElementsByTagName('textarea')[0];
|
|
// Now the textarea should have focus.
|
|
expect(document.activeElement, textarea);
|
|
expect(editingElement.domElement, textarea);
|
|
|
|
textarea.value = 'foo\nbar';
|
|
textarea.dispatchEvent(Event.eventType('Event', 'input'));
|
|
textarea.setSelectionRange(4, 6);
|
|
textarea.dispatchEvent(Event.eventType('Event', 'selectionchange'));
|
|
// Can read textarea state correctly (and preserves new lines).
|
|
expect(
|
|
lastEditingState,
|
|
EditingState(text: 'foo\nbar', baseOffset: 4, extentOffset: 6),
|
|
);
|
|
|
|
// Can set textarea state correctly (and preserves new lines).
|
|
editingElement.setEditingState(
|
|
EditingState(text: 'bar\nbaz', baseOffset: 2, extentOffset: 7));
|
|
checkTextAreaEditingState(textarea, 'bar\nbaz', 2, 7);
|
|
|
|
editingElement.disable();
|
|
// The textarea should be cleaned up.
|
|
expect(document.getElementsByTagName('textarea'), hasLength(0));
|
|
// The focus is back to the body.
|
|
expect(document.activeElement, document.body);
|
|
|
|
// There should be no input action.
|
|
expect(lastInputAction, isNull);
|
|
});
|
|
|
|
test('Same instance can be re-enabled with different config', () {
|
|
// Make sure there's nothing in the DOM yet.
|
|
expect(document.getElementsByTagName('input'), hasLength(0));
|
|
expect(document.getElementsByTagName('textarea'), hasLength(0));
|
|
|
|
// Use single-line config and expect an `<input>` to be created.
|
|
editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(document.getElementsByTagName('input'), hasLength(1));
|
|
expect(document.getElementsByTagName('textarea'), hasLength(0));
|
|
|
|
// Disable and check that all DOM elements were removed.
|
|
editingElement.disable();
|
|
expect(document.getElementsByTagName('input'), hasLength(0));
|
|
expect(document.getElementsByTagName('textarea'), hasLength(0));
|
|
|
|
// Use multi-line config and expect an `<textarea>` to be created.
|
|
editingElement.enable(
|
|
multilineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(document.getElementsByTagName('input'), hasLength(0));
|
|
expect(document.getElementsByTagName('textarea'), hasLength(1));
|
|
|
|
// Disable again and check that all DOM elements were removed.
|
|
editingElement.disable();
|
|
expect(document.getElementsByTagName('input'), hasLength(0));
|
|
expect(document.getElementsByTagName('textarea'), hasLength(0));
|
|
|
|
// There should be no input action.
|
|
expect(lastInputAction, isNull);
|
|
});
|
|
|
|
test('Triggers input action', () {
|
|
final InputConfiguration config =
|
|
InputConfiguration(inputAction: 'TextInputAction.done');
|
|
editingElement.enable(
|
|
config,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
|
|
// No input action so far.
|
|
expect(lastInputAction, isNull);
|
|
|
|
dispatchKeyboardEvent(
|
|
editingElement.domElement,
|
|
'keydown',
|
|
keyCode: _kReturnKeyCode,
|
|
);
|
|
expect(lastInputAction, 'TextInputAction.done');
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: browserEngine == BrowserEngine.edge);
|
|
|
|
test('Does not trigger input action in multi-line mode', () {
|
|
final InputConfiguration config = InputConfiguration(
|
|
inputType: EngineInputType.multiline,
|
|
inputAction: 'TextInputAction.done',
|
|
);
|
|
editingElement.enable(
|
|
config,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
|
|
// No input action so far.
|
|
expect(lastInputAction, isNull);
|
|
|
|
final KeyboardEvent event = dispatchKeyboardEvent(
|
|
editingElement.domElement,
|
|
'keydown',
|
|
keyCode: _kReturnKeyCode,
|
|
);
|
|
|
|
// Still no input action.
|
|
expect(lastInputAction, isNull);
|
|
// And default behavior of keyboard event shouldn't have been prevented.
|
|
expect(event.defaultPrevented, isFalse);
|
|
});
|
|
|
|
test('globally positions and sizes its DOM element', () {
|
|
editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(editingElement.isEnabled, isTrue);
|
|
|
|
// No geometry should be set until setEditableSizeAndTransform is called.
|
|
expect(editingElement.domElement.style.transform, '');
|
|
expect(editingElement.domElement.style.width, '');
|
|
expect(editingElement.domElement.style.height, '');
|
|
|
|
testTextEditing.setEditableSizeAndTransform(EditableTextGeometry(
|
|
width: 13,
|
|
height: 12,
|
|
globalTransform: Matrix4.translationValues(14, 15, 0).storage,
|
|
));
|
|
|
|
// setEditableSizeAndTransform calls placeElement, so expecting geometry to be applied.
|
|
expect(editingElement.domElement.style.transform,
|
|
'matrix(1, 0, 0, 1, 14, 15)');
|
|
expect(editingElement.domElement.style.width, '13px');
|
|
expect(editingElement.domElement.style.height, '12px');
|
|
});
|
|
});
|
|
|
|
group('$SemanticsTextEditingStrategy', () {
|
|
InputElement testInputElement;
|
|
HybridTextEditing testTextEditing;
|
|
|
|
setUp(() {
|
|
testInputElement = InputElement();
|
|
testTextEditing = HybridTextEditing();
|
|
editingElement = GloballyPositionedTextEditingStrategy(testTextEditing);
|
|
});
|
|
|
|
tearDown(() {
|
|
testInputElement = null;
|
|
});
|
|
|
|
test('Does not accept dom elements of a wrong type', () {
|
|
// A regular <span> shouldn't be accepted.
|
|
final HtmlElement span = SpanElement();
|
|
expect(
|
|
() => SemanticsTextEditingStrategy(HybridTextEditing(), span),
|
|
throwsAssertionError,
|
|
);
|
|
});
|
|
|
|
test('Re-acquire focus', () {
|
|
editingElement =
|
|
SemanticsTextEditingStrategy(HybridTextEditing(), testInputElement);
|
|
|
|
expect(document.activeElement, document.body);
|
|
|
|
document.body.append(testInputElement);
|
|
editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(document.activeElement, testInputElement);
|
|
|
|
// The input should refocus after blur.
|
|
editingElement.domElement.blur();
|
|
expect(document.activeElement, editingElement.domElement);
|
|
|
|
editingElement.disable();
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: (browserEngine == BrowserEngine.webkit ||
|
|
browserEngine == BrowserEngine.edge ||
|
|
browserEngine == BrowserEngine.firefox));
|
|
|
|
test('Does not dispose and recreate dom elements in persistent mode', () {
|
|
editingElement =
|
|
SemanticsTextEditingStrategy(HybridTextEditing(), testInputElement);
|
|
|
|
// The DOM element should've been eagerly created.
|
|
expect(testInputElement, isNotNull);
|
|
// But doesn't have focus.
|
|
expect(document.activeElement, document.body);
|
|
|
|
// Can't enable before the input element is inserted into the DOM.
|
|
expect(
|
|
() => editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
),
|
|
throwsAssertionError,
|
|
);
|
|
|
|
document.body.append(testInputElement);
|
|
editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
expect(document.activeElement, editingElement.domElement);
|
|
// It doesn't create a new DOM element.
|
|
expect(editingElement.domElement, testInputElement);
|
|
|
|
editingElement.disable();
|
|
// It doesn't remove the DOM element.
|
|
expect(editingElement.domElement, testInputElement);
|
|
expect(document.body.contains(editingElement.domElement), isTrue);
|
|
// The textArea does not lose focus.
|
|
// Even though this passes on manual tests it does not work on
|
|
// Firefox automated unit tests.
|
|
if (browserEngine != BrowserEngine.firefox) {
|
|
expect(document.activeElement, editingElement.domElement);
|
|
}
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: (browserEngine == BrowserEngine.webkit ||
|
|
browserEngine == BrowserEngine.edge));
|
|
|
|
test('Refocuses when setting editing state', () {
|
|
editingElement =
|
|
SemanticsTextEditingStrategy(HybridTextEditing(), testInputElement);
|
|
|
|
document.body.append(testInputElement);
|
|
editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
// The input will have focus after editing state is set.
|
|
editingElement.setEditingState(EditingState(text: 'foo'));
|
|
expect(document.activeElement, testInputElement);
|
|
|
|
editingElement.disable();
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: (browserEngine == BrowserEngine.webkit ||
|
|
browserEngine == BrowserEngine.edge));
|
|
|
|
test('Works in multi-line mode', () {
|
|
final TextAreaElement textarea = TextAreaElement();
|
|
editingElement =
|
|
SemanticsTextEditingStrategy(HybridTextEditing(), textarea);
|
|
|
|
expect(editingElement.domElement, textarea);
|
|
expect(document.activeElement, document.body);
|
|
|
|
// Can't enable before the textarea is inserted into the DOM.
|
|
expect(
|
|
() => editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
),
|
|
throwsAssertionError,
|
|
);
|
|
|
|
document.body.append(textarea);
|
|
editingElement.enable(
|
|
multilineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
// Focuses the textarea.
|
|
expect(document.activeElement, textarea);
|
|
|
|
textarea.blur();
|
|
// The textArea does not lose focus.
|
|
// Even though this passes on manual tests it does not work on
|
|
// Firefox automated unit tests.
|
|
if (browserEngine != BrowserEngine.firefox) {
|
|
expect(document.activeElement, textarea);
|
|
}
|
|
|
|
editingElement.disable();
|
|
// It doesn't remove the textarea from the DOM.
|
|
expect(document.body.contains(editingElement.domElement), isTrue);
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: (browserEngine == BrowserEngine.webkit ||
|
|
browserEngine == BrowserEngine.edge));
|
|
|
|
test('Does not position or size its DOM element', () {
|
|
editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
testTextEditing.setEditableSizeAndTransform(EditableTextGeometry(
|
|
height: 12,
|
|
width: 13,
|
|
globalTransform: Matrix4.translationValues(14, 15, 0).storage,
|
|
));
|
|
|
|
void checkPlacementIsEmpty() {
|
|
expect(editingElement.domElement.style.transform, '');
|
|
expect(editingElement.domElement.style.width, '');
|
|
expect(editingElement.domElement.style.height, '');
|
|
}
|
|
|
|
checkPlacementIsEmpty();
|
|
editingElement.placeElement();
|
|
checkPlacementIsEmpty();
|
|
});
|
|
});
|
|
|
|
group('$HybridTextEditing', () {
|
|
HybridTextEditing textEditing;
|
|
final PlatformMessagesSpy spy = PlatformMessagesSpy();
|
|
|
|
int clientId = 0;
|
|
|
|
/// Emulates sending of a message by the framework to the engine.
|
|
void sendFrameworkMessage(dynamic message) {
|
|
textEditing.channel.handleTextInput(message, (ByteData data) {});
|
|
}
|
|
|
|
/// Sends the necessary platform messages to activate a text field and show
|
|
/// the keyboard.
|
|
///
|
|
/// Returns the `clientId` used in the platform message.
|
|
int showKeyboard(
|
|
{String inputType, String inputAction, bool decimal = false}) {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient',
|
|
<dynamic>[
|
|
++clientId,
|
|
createFlutterConfig(inputType,
|
|
inputAction: inputAction, decimal: decimal),
|
|
],
|
|
);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
return clientId;
|
|
}
|
|
|
|
void hideKeyboard() {
|
|
const MethodCall hide = MethodCall('TextInput.hide');
|
|
sendFrameworkMessage(codec.encodeMethodCall(hide));
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
}
|
|
|
|
String getEditingInputMode() {
|
|
return textEditing.editingElement.domElement.getAttribute('inputmode');
|
|
}
|
|
|
|
setUp(() {
|
|
textEditing = HybridTextEditing();
|
|
spy.setUp();
|
|
});
|
|
|
|
tearDown(() {
|
|
spy.tearDown();
|
|
if (textEditing.isEditing) {
|
|
textEditing.stopEditing();
|
|
}
|
|
textEditing = null;
|
|
});
|
|
|
|
test('setClient, show, setEditingState, hide', () {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
// Editing shouldn't have started yet.
|
|
expect(document.activeElement, document.body);
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
checkInputEditingState(textEditing.editingElement.domElement, '', 0, 0);
|
|
|
|
const MethodCall setEditingState =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'abcd',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'abcd', 2, 3);
|
|
|
|
const MethodCall hide = MethodCall('TextInput.hide');
|
|
sendFrameworkMessage(codec.encodeMethodCall(hide));
|
|
|
|
// Text editing should've stopped.
|
|
expect(document.activeElement, document.body);
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any messages.
|
|
expect(spy.messages, isEmpty);
|
|
});
|
|
|
|
test('setClient, setEditingState, show, clearClient', () {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall setEditingState =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'abcd',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
|
|
// Editing shouldn't have started yet.
|
|
expect(document.activeElement, document.body);
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'abcd', 2, 3);
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
|
|
expect(document.activeElement, document.body);
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any messages.
|
|
expect(spy.messages, isEmpty);
|
|
});
|
|
|
|
test('setClient, setEditingState, show, updateConfig, clearClient', () {
|
|
final MethodCall setClient = MethodCall('TextInput.setClient', <dynamic>[
|
|
123,
|
|
createFlutterConfig('text', readOnly: true),
|
|
]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall setEditingState = MethodCall(
|
|
'TextInput.setEditingState',
|
|
<String, dynamic>{
|
|
'text': 'abcd',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
},
|
|
);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
final HtmlElement element = textEditing.editingElement.domElement;
|
|
expect(element.getAttribute('readonly'), 'readonly');
|
|
|
|
// Update the read-only config.
|
|
final MethodCall updateConfig = MethodCall(
|
|
'TextInput.updateConfig',
|
|
createFlutterConfig('text', readOnly: false),
|
|
);
|
|
sendFrameworkMessage(codec.encodeMethodCall(updateConfig));
|
|
|
|
expect(element.hasAttribute('readonly'), isFalse);
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any messages.
|
|
expect(spy.messages, isEmpty);
|
|
});
|
|
|
|
test('do not close connection on blur', () async {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall setEditingState =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'abcd',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
|
|
// Editing shouldn't have started yet.
|
|
expect(document.activeElement, document.body);
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'abcd', 2, 3);
|
|
|
|
// DOM element is blurred.
|
|
textEditing.editingElement.domElement.blur();
|
|
|
|
expect(spy.messages, hasLength(0));
|
|
|
|
// DOM element still has focus.
|
|
// Even though this passes on manual tests it does not work on
|
|
// Firefox automated unit tests.
|
|
if (browserEngine != BrowserEngine.firefox) {
|
|
expect(document.activeElement, textEditing.editingElement.domElement);
|
|
}
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: (browserEngine == BrowserEngine.webkit ||
|
|
browserEngine == BrowserEngine.edge));
|
|
|
|
test('finishAutofillContext closes connection no autofill element',
|
|
() async {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall setEditingState =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'abcd',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
|
|
// Editing shouldn't have started yet.
|
|
expect(document.activeElement, document.body);
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'abcd', 2, 3);
|
|
|
|
const MethodCall finishAutofillContext =
|
|
MethodCall('TextInput.finishAutofillContext', false);
|
|
sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext));
|
|
|
|
expect(spy.messages, hasLength(1));
|
|
expect(spy.messages[0].channel, 'flutter/textinput');
|
|
expect(spy.messages[0].methodName, 'TextInputClient.onConnectionClosed');
|
|
expect(
|
|
spy.messages[0].methodArguments,
|
|
<dynamic>[
|
|
123, // Client ID
|
|
],
|
|
);
|
|
spy.messages.clear();
|
|
// Input element is removed from DOM.
|
|
expect(document.getElementsByTagName('input'), hasLength(0));
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: (browserEngine == BrowserEngine.webkit ||
|
|
browserEngine == BrowserEngine.edge));
|
|
|
|
test('finishAutofillContext removes form from DOM', () async {
|
|
// 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));
|
|
|
|
// Form is added to DOM.
|
|
expect(document.getElementsByTagName('form'), isNotEmpty);
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any messages.
|
|
expect(spy.messages, isEmpty);
|
|
// Form stays on the DOM until autofill context is finalized.
|
|
expect(document.getElementsByTagName('form'), isNotEmpty);
|
|
expect(formsOnTheDom, hasLength(1));
|
|
|
|
const MethodCall finishAutofillContext =
|
|
MethodCall('TextInput.finishAutofillContext', false);
|
|
sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext));
|
|
|
|
// Form element is removed from DOM.
|
|
expect(document.getElementsByTagName('form'), hasLength(0));
|
|
expect(formsOnTheDom, hasLength(0));
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: (browserEngine == BrowserEngine.webkit ||
|
|
browserEngine == BrowserEngine.edge));
|
|
|
|
test('finishAutofillContext with save submits forms', () async {
|
|
// 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));
|
|
|
|
// Form is added to DOM.
|
|
expect(document.getElementsByTagName('form'), isNotEmpty);
|
|
FormElement formElement = document.getElementsByTagName('form')[0];
|
|
final Completer<bool> submittedForm = Completer<bool>();
|
|
formElement.addEventListener(
|
|
'submit', (event) => submittedForm.complete(true));
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
|
|
const MethodCall finishAutofillContext =
|
|
MethodCall('TextInput.finishAutofillContext', true);
|
|
sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext));
|
|
|
|
// `submit` action is called on form.
|
|
await expectLater(await submittedForm.future, true);
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: (browserEngine == BrowserEngine.webkit ||
|
|
browserEngine == BrowserEngine.edge));
|
|
|
|
test('forms submits for focused input', () async {
|
|
// 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));
|
|
|
|
// Form is added to DOM.
|
|
expect(document.getElementsByTagName('form'), isNotEmpty);
|
|
FormElement formElement = document.getElementsByTagName('form')[0];
|
|
final Completer<bool> submittedForm = Completer<bool>();
|
|
formElement.addEventListener(
|
|
'submit', (event) => submittedForm.complete(true));
|
|
|
|
// Clear client is not called. The used requested context to be finalized.
|
|
const MethodCall finishAutofillContext =
|
|
MethodCall('TextInput.finishAutofillContext', true);
|
|
sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext));
|
|
|
|
// Connection is closed by the engine.
|
|
expect(spy.messages, hasLength(1));
|
|
expect(spy.messages[0].channel, 'flutter/textinput');
|
|
expect(spy.messages[0].methodName, 'TextInputClient.onConnectionClosed');
|
|
|
|
// `submit` action is called on form.
|
|
await expectLater(await submittedForm.future, true);
|
|
// Form element is removed from DOM.
|
|
expect(document.getElementsByTagName('form'), hasLength(0));
|
|
expect(formsOnTheDom, hasLength(0));
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: (browserEngine == BrowserEngine.webkit ||
|
|
browserEngine == BrowserEngine.edge));
|
|
|
|
test('setClient, setEditingState, show, setClient', () {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall setEditingState =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'abcd',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
|
|
// Editing shouldn't have started yet.
|
|
expect(document.activeElement, document.body);
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'abcd', 2, 3);
|
|
|
|
final MethodCall setClient2 = MethodCall(
|
|
'TextInput.setClient', <dynamic>[567, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient2));
|
|
|
|
// Receiving another client via setClient should stop editing, hence
|
|
// should remove the previous active element.
|
|
expect(document.activeElement, document.body);
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any messages.
|
|
expect(spy.messages, isEmpty);
|
|
|
|
hideKeyboard();
|
|
});
|
|
|
|
test('setClient, setEditingState, show, setEditingState, clearClient', () {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
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));
|
|
|
|
const MethodCall setEditingState2 =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'xyz',
|
|
'selectionBase': 0,
|
|
'selectionExtent': 2,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState2));
|
|
|
|
// The second [setEditingState] should override the first one.
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'xyz', 0, 2);
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any messages.
|
|
expect(spy.messages, isEmpty);
|
|
});
|
|
|
|
test(
|
|
'singleTextField Autofill: setClient, setEditingState, show, '
|
|
'setSizeAndTransform, 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));
|
|
|
|
final MethodCall setSizeAndTransform =
|
|
configureSetSizeAndTransformMethodCall(150, 50,
|
|
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
|
|
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
|
|
|
// The second [setEditingState] should override the first one.
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'abcd', 2, 3);
|
|
|
|
final FormElement formElement = document.getElementsByTagName('form')[0];
|
|
// The form has one input element and one submit button.
|
|
expect(formElement.childNodes, hasLength(2));
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any messages.
|
|
expect(spy.messages, isEmpty);
|
|
// Form stays on the DOM until autofill context is finalized.
|
|
expect(document.getElementsByTagName('form'), isNotEmpty);
|
|
expect(formsOnTheDom, hasLength(1));
|
|
});
|
|
|
|
test(
|
|
'singleTextField Autofill setEditableSizeAndTransform preserves'
|
|
'editing state', () {
|
|
// 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));
|
|
|
|
final InputElement inputElement =
|
|
textEditing.editingElement.domElement as InputElement;
|
|
expect(inputElement.value, 'abcd');
|
|
if (!(browserEngine == BrowserEngine.webkit &&
|
|
operatingSystem == OperatingSystem.macOs)) {
|
|
// In Safari Desktop Autofill menu appears as soon as an element is
|
|
// focused, therefore the input element is only focused after the
|
|
// location is received.
|
|
expect(document.activeElement, inputElement);
|
|
expect(inputElement.selectionStart, 2);
|
|
expect(inputElement.selectionEnd, 3);
|
|
}
|
|
|
|
// The transform is changed. For example after a validation error, red
|
|
// line appeared under the input field.
|
|
final MethodCall setSizeAndTransform =
|
|
configureSetSizeAndTransformMethodCall(150, 50,
|
|
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
|
|
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
|
|
|
// Check the element still has focus. User can keep editing.
|
|
expect(document.activeElement, textEditing.editingElement.domElement);
|
|
|
|
// Check the cursor location is the same.
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'abcd', 2, 3);
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any messages.
|
|
expect(spy.messages, isEmpty);
|
|
// Form stays on the DOM until autofill context is finalized.
|
|
expect(document.getElementsByTagName('form'), isNotEmpty);
|
|
expect(formsOnTheDom, hasLength(1));
|
|
});
|
|
|
|
test(
|
|
'multiTextField Autofill: setClient, setEditingState, show, '
|
|
'setSizeAndTransform 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));
|
|
|
|
final MethodCall setSizeAndTransform =
|
|
configureSetSizeAndTransformMethodCall(150, 50,
|
|
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
|
|
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
|
|
|
// The second [setEditingState] should override the first one.
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'abcd', 2, 3);
|
|
|
|
final FormElement formElement = document.getElementsByTagName('form')[0];
|
|
// The form has 4 input elements and one submit button.
|
|
expect(formElement.childNodes, hasLength(5));
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any messages.
|
|
expect(spy.messages, isEmpty);
|
|
// Form stays on the DOM until autofill context is finalized.
|
|
expect(document.getElementsByTagName('form'), isNotEmpty);
|
|
expect(formsOnTheDom, hasLength(1));
|
|
});
|
|
|
|
test('No capitilization: setClient, setEditingState, show', () {
|
|
// Create a configuration with an AutofillGroup of four text fields.
|
|
final Map<String, dynamic> capitilizeWordsConfig = createFlutterConfig(
|
|
'text',
|
|
textCapitalization: 'TextCapitalization.none');
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, capitilizeWordsConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall setEditingState1 =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': '',
|
|
'selectionBase': 0,
|
|
'selectionExtent': 0,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
spy.messages.clear();
|
|
|
|
// Test for mobile Safari. `sentences` is the default attribute for
|
|
// mobile browsers. Check if `off` is added to the input element.
|
|
if (browserEngine == BrowserEngine.webkit &&
|
|
operatingSystem == OperatingSystem.iOs) {
|
|
expect(
|
|
textEditing.editingElement.domElement
|
|
.getAttribute('autocapitalize'),
|
|
'off');
|
|
} else {
|
|
expect(
|
|
textEditing.editingElement.domElement
|
|
.getAttribute('autocapitalize'),
|
|
isNull);
|
|
}
|
|
|
|
spy.messages.clear();
|
|
hideKeyboard();
|
|
});
|
|
|
|
test('All characters capitilization: setClient, setEditingState, show', () {
|
|
// Create a configuration with an AutofillGroup of four text fields.
|
|
final Map<String, dynamic> capitilizeWordsConfig = createFlutterConfig(
|
|
'text',
|
|
textCapitalization: 'TextCapitalization.characters');
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, capitilizeWordsConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall setEditingState1 =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': '',
|
|
'selectionBase': 0,
|
|
'selectionExtent': 0,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
spy.messages.clear();
|
|
|
|
// Test for mobile Safari.
|
|
if (browserEngine == BrowserEngine.webkit &&
|
|
operatingSystem == OperatingSystem.iOs) {
|
|
expect(
|
|
textEditing.editingElement.domElement
|
|
.getAttribute('autocapitalize'),
|
|
'characters');
|
|
}
|
|
|
|
spy.messages.clear();
|
|
hideKeyboard();
|
|
});
|
|
|
|
test(
|
|
'setClient, setEditableSizeAndTransform, setStyle, setEditingState, show, clearClient',
|
|
() {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
final MethodCall setSizeAndTransform =
|
|
configureSetSizeAndTransformMethodCall(150, 50,
|
|
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
|
|
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
|
|
|
final MethodCall setStyle =
|
|
configureSetStyleMethodCall(12, 'sans-serif', 4, 4, 1);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setStyle));
|
|
|
|
const MethodCall setEditingState =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'abcd',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
final HtmlElement domElement = textEditing.editingElement.domElement;
|
|
|
|
checkInputEditingState(domElement, 'abcd', 2, 3);
|
|
|
|
// Check if the location and styling is correct.
|
|
expect(
|
|
domElement.getBoundingClientRect(),
|
|
Rectangle<double>.fromPoints(const Point<double>(10.0, 20.0),
|
|
const Point<double>(160.0, 70.0)));
|
|
expect(domElement.style.transform,
|
|
'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 20, 30, 1)');
|
|
expect(textEditing.editingElement.domElement.style.font,
|
|
'500 12px sans-serif');
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any messages.
|
|
expect(spy.messages, isEmpty);
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
skip: browserEngine == BrowserEngine.webkit);
|
|
|
|
test(
|
|
'setClient, show, setEditableSizeAndTransform, setStyle, setEditingState, clearClient',
|
|
() {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
final MethodCall setSizeAndTransform =
|
|
configureSetSizeAndTransformMethodCall(
|
|
150,
|
|
50,
|
|
Matrix4.translationValues(
|
|
10.0,
|
|
20.0,
|
|
30.0,
|
|
).storage.toList());
|
|
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
|
|
|
final MethodCall setStyle =
|
|
configureSetStyleMethodCall(12, 'sans-serif', 4, 4, 1);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setStyle));
|
|
|
|
const MethodCall setEditingState =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'abcd',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
|
|
final HtmlElement domElement = textEditing.editingElement.domElement;
|
|
|
|
checkInputEditingState(domElement, 'abcd', 2, 3);
|
|
|
|
// Check if the position is correct.
|
|
expect(
|
|
domElement.getBoundingClientRect(),
|
|
Rectangle<double>.fromPoints(
|
|
const Point<double>(10.0, 20.0), const Point<double>(160.0, 70.0)),
|
|
);
|
|
expect(
|
|
domElement.style.transform,
|
|
'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 20, 30, 1)',
|
|
);
|
|
expect(
|
|
textEditing.editingElement.domElement.style.font,
|
|
'500 12px sans-serif',
|
|
);
|
|
|
|
// For `blink` and `webkit` browser engines the overlay would be hidden.
|
|
if (browserEngine == BrowserEngine.blink ||
|
|
browserEngine == BrowserEngine.webkit) {
|
|
expect(textEditing.editingElement.domElement.classes,
|
|
contains('transparentTextEditing'));
|
|
} else {
|
|
expect(
|
|
textEditing.editingElement.domElement.classes.any(
|
|
(element) => element.toString() == 'transparentTextEditing'),
|
|
isFalse);
|
|
}
|
|
|
|
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
|
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
skip: browserEngine == BrowserEngine.webkit);
|
|
|
|
test('input font set succesfully with null fontWeightIndex', () {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
final MethodCall setSizeAndTransform =
|
|
configureSetSizeAndTransformMethodCall(150, 50,
|
|
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
|
|
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
|
|
|
final MethodCall setStyle = configureSetStyleMethodCall(
|
|
12, 'sans-serif', 4, null /* fontWeightIndex */, 1);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setStyle));
|
|
|
|
const MethodCall setEditingState =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'abcd',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
final HtmlElement domElement = textEditing.editingElement.domElement;
|
|
|
|
checkInputEditingState(domElement, 'abcd', 2, 3);
|
|
|
|
// Check if the location and styling is correct.
|
|
expect(
|
|
domElement.getBoundingClientRect(),
|
|
Rectangle<double>.fromPoints(const Point<double>(10.0, 20.0),
|
|
const Point<double>(160.0, 70.0)));
|
|
expect(domElement.style.transform,
|
|
'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 20, 30, 1)');
|
|
expect(
|
|
textEditing.editingElement.domElement.style.font, '12px sans-serif');
|
|
|
|
hideKeyboard();
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
|
skip: browserEngine == BrowserEngine.webkit);
|
|
|
|
test('Canonicalizes font family', () {
|
|
showKeyboard();
|
|
|
|
final HtmlElement input = textEditing.editingElement.domElement;
|
|
|
|
MethodCall setStyle;
|
|
|
|
setStyle = configureSetStyleMethodCall(12, 'sans-serif', 4, 4, 1);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setStyle));
|
|
expect(input.style.fontFamily, canonicalizeFontFamily('sans-serif'));
|
|
|
|
setStyle = configureSetStyleMethodCall(12, '.SF Pro Text', 4, 4, 1);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setStyle));
|
|
expect(input.style.fontFamily, canonicalizeFontFamily('.SF Pro Text'));
|
|
|
|
setStyle = configureSetStyleMethodCall(12, 'foo bar baz', 4, 4, 1);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setStyle));
|
|
expect(input.style.fontFamily, canonicalizeFontFamily('foo bar baz'));
|
|
|
|
hideKeyboard();
|
|
});
|
|
|
|
test(
|
|
'negative base offset and selection extent values in editing state is handled',
|
|
() {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall setEditingState1 =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'xyz',
|
|
'selectionBase': 1,
|
|
'selectionExtent': 2,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
// Check if the selection range is correct.
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'xyz', 1, 2);
|
|
|
|
const MethodCall setEditingState2 =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'xyz',
|
|
'selectionBase': -1,
|
|
'selectionExtent': -1,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState2));
|
|
|
|
// The negative offset values are applied to the dom element as 0.
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'xyz', 0, 0);
|
|
|
|
hideKeyboard();
|
|
});
|
|
|
|
test('Syncs the editing state back to Flutter', () {
|
|
final MethodCall setClient = MethodCall(
|
|
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
const MethodCall setEditingState =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'abcd',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
final InputElement input = textEditing.editingElement.domElement;
|
|
|
|
input.value = 'something';
|
|
input.dispatchEvent(Event.eventType('Event', 'input'));
|
|
|
|
expect(spy.messages, hasLength(1));
|
|
expect(spy.messages[0].channel, 'flutter/textinput');
|
|
expect(spy.messages[0].methodName, 'TextInputClient.updateEditingState');
|
|
expect(
|
|
spy.messages[0].methodArguments,
|
|
<dynamic>[
|
|
123, // Client ID
|
|
<String, dynamic>{
|
|
'text': 'something',
|
|
'selectionBase': 9,
|
|
'selectionExtent': 9
|
|
}
|
|
],
|
|
);
|
|
spy.messages.clear();
|
|
|
|
input.setSelectionRange(2, 5);
|
|
if (browserEngine == BrowserEngine.firefox) {
|
|
Event keyup = KeyboardEvent('keyup');
|
|
textEditing.editingElement.domElement.dispatchEvent(keyup);
|
|
} else {
|
|
document.dispatchEvent(Event.eventType('Event', 'selectionchange'));
|
|
}
|
|
|
|
expect(spy.messages, hasLength(1));
|
|
expect(spy.messages[0].channel, 'flutter/textinput');
|
|
expect(spy.messages[0].methodName, 'TextInputClient.updateEditingState');
|
|
expect(
|
|
spy.messages[0].methodArguments,
|
|
<dynamic>[
|
|
123, // Client ID
|
|
<String, dynamic>{
|
|
'text': 'something',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 5
|
|
}
|
|
],
|
|
);
|
|
spy.messages.clear();
|
|
|
|
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));
|
|
|
|
final MethodCall setSizeAndTransform =
|
|
configureSetSizeAndTransformMethodCall(150, 50,
|
|
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
|
|
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
|
|
|
// The second [setEditingState] should override the first one.
|
|
checkInputEditingState(
|
|
textEditing.editingElement.domElement, 'abcd', 2, 3);
|
|
|
|
final FormElement formElement = document.getElementsByTagName('form')[0];
|
|
// The form has 4 input elements and one submit button.
|
|
expect(formElement.childNodes, hasLength(5));
|
|
|
|
// 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]);
|
|
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
|
|
|
// Editing shouldn't have started yet.
|
|
expect(document.activeElement, document.body);
|
|
|
|
const MethodCall show = MethodCall('TextInput.show');
|
|
sendFrameworkMessage(codec.encodeMethodCall(show));
|
|
|
|
final TextAreaElement textarea = textEditing.editingElement.domElement;
|
|
checkTextAreaEditingState(textarea, '', 0, 0);
|
|
|
|
// Can set editing state and preserve new lines.
|
|
const MethodCall setEditingState =
|
|
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
|
'text': 'foo\nbar',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 3,
|
|
});
|
|
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
|
|
checkTextAreaEditingState(textarea, 'foo\nbar', 2, 3);
|
|
|
|
textarea.value = 'something\nelse';
|
|
|
|
textarea.dispatchEvent(Event.eventType('Event', 'input'));
|
|
textarea.setSelectionRange(2, 5);
|
|
if (browserEngine == BrowserEngine.firefox) {
|
|
textEditing.editingElement.domElement
|
|
.dispatchEvent(KeyboardEvent('keyup'));
|
|
} else {
|
|
document.dispatchEvent(Event.eventType('Event', 'selectionchange'));
|
|
}
|
|
|
|
// Two messages should've been sent. One for the 'input' event and one for
|
|
// the 'selectionchange' event.
|
|
expect(spy.messages, hasLength(2));
|
|
|
|
expect(spy.messages[0].channel, 'flutter/textinput');
|
|
expect(spy.messages[0].methodName, 'TextInputClient.updateEditingState');
|
|
expect(
|
|
spy.messages[0].methodArguments,
|
|
<dynamic>[
|
|
123, // Client ID
|
|
<String, dynamic>{
|
|
'text': 'something\nelse',
|
|
'selectionBase': 14,
|
|
'selectionExtent': 14,
|
|
}
|
|
],
|
|
);
|
|
|
|
expect(spy.messages[1].channel, 'flutter/textinput');
|
|
expect(spy.messages[1].methodName, 'TextInputClient.updateEditingState');
|
|
expect(
|
|
spy.messages[1].methodArguments,
|
|
<dynamic>[
|
|
123, // Client ID
|
|
<String, dynamic>{
|
|
'text': 'something\nelse',
|
|
'selectionBase': 2,
|
|
'selectionExtent': 5,
|
|
}
|
|
],
|
|
);
|
|
spy.messages.clear();
|
|
|
|
const MethodCall hide = MethodCall('TextInput.hide');
|
|
sendFrameworkMessage(codec.encodeMethodCall(hide));
|
|
|
|
// Text editing should've stopped.
|
|
expect(document.activeElement, document.body);
|
|
|
|
// Confirm that [HybridTextEditing] didn't send any more messages.
|
|
expect(spy.messages, isEmpty);
|
|
});
|
|
|
|
test('sets correct input type in Android', () {
|
|
debugOperatingSystemOverride = OperatingSystem.android;
|
|
debugBrowserEngineOverride = BrowserEngine.blink;
|
|
|
|
/// During initialization [HybridTextEditing] will pick the correct
|
|
/// text editing strategy for [OperatingSystem.android].
|
|
textEditing = HybridTextEditing();
|
|
|
|
showKeyboard(inputType: 'text');
|
|
expect(getEditingInputMode(), 'text');
|
|
|
|
showKeyboard(inputType: 'number');
|
|
expect(getEditingInputMode(), 'numeric');
|
|
|
|
showKeyboard(inputType: 'number', decimal: false);
|
|
expect(getEditingInputMode(), 'numeric');
|
|
|
|
showKeyboard(inputType: 'number', decimal: true);
|
|
expect(getEditingInputMode(), 'decimal');
|
|
|
|
showKeyboard(inputType: 'phone');
|
|
expect(getEditingInputMode(), 'tel');
|
|
|
|
showKeyboard(inputType: 'emailAddress');
|
|
expect(getEditingInputMode(), 'email');
|
|
|
|
showKeyboard(inputType: 'url');
|
|
expect(getEditingInputMode(), 'url');
|
|
|
|
hideKeyboard();
|
|
});
|
|
|
|
test('sets correct input type in iOS', () {
|
|
// Test on ios-safari only.
|
|
if (browserEngine == BrowserEngine.webkit &&
|
|
operatingSystem == OperatingSystem.iOs) {
|
|
/// During initialization [HybridTextEditing] will pick the correct
|
|
/// text editing strategy for [OperatingSystem.iOs].
|
|
textEditing = HybridTextEditing();
|
|
|
|
showKeyboard(inputType: 'text');
|
|
expect(getEditingInputMode(), 'text');
|
|
|
|
showKeyboard(inputType: 'number');
|
|
expect(getEditingInputMode(), 'numeric');
|
|
|
|
showKeyboard(inputType: 'number', decimal: false);
|
|
expect(getEditingInputMode(), 'numeric');
|
|
|
|
showKeyboard(inputType: 'number', decimal: true);
|
|
expect(getEditingInputMode(), 'decimal');
|
|
|
|
showKeyboard(inputType: 'phone');
|
|
expect(getEditingInputMode(), 'tel');
|
|
|
|
showKeyboard(inputType: 'emailAddress');
|
|
expect(getEditingInputMode(), 'email');
|
|
|
|
showKeyboard(inputType: 'url');
|
|
expect(getEditingInputMode(), 'url');
|
|
|
|
hideKeyboard();
|
|
}
|
|
});
|
|
|
|
test('sends the correct input action as a platform message', () {
|
|
final int clientId = showKeyboard(
|
|
inputType: 'text',
|
|
inputAction: 'TextInputAction.next',
|
|
);
|
|
|
|
// There should be no input action yet.
|
|
expect(lastInputAction, isNull);
|
|
|
|
dispatchKeyboardEvent(
|
|
textEditing.editingElement.domElement,
|
|
'keydown',
|
|
keyCode: _kReturnKeyCode,
|
|
);
|
|
|
|
expect(spy.messages, hasLength(1));
|
|
expect(spy.messages[0].channel, 'flutter/textinput');
|
|
expect(spy.messages[0].methodName, 'TextInputClient.performAction');
|
|
expect(
|
|
spy.messages[0].methodArguments,
|
|
<dynamic>[clientId, 'TextInputAction.next'],
|
|
);
|
|
},
|
|
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
|
skip: browserEngine == BrowserEngine.edge);
|
|
|
|
test('does not send input action in multi-line mode', () {
|
|
showKeyboard(
|
|
inputType: 'multiline',
|
|
inputAction: 'TextInputAction.next',
|
|
);
|
|
|
|
final KeyboardEvent event = dispatchKeyboardEvent(
|
|
textEditing.editingElement.domElement,
|
|
'keydown',
|
|
keyCode: _kReturnKeyCode,
|
|
);
|
|
|
|
// No input action and no platform message have been sent.
|
|
expect(spy.messages, isEmpty);
|
|
// And default behavior of keyboard event shouldn't have been prevented.
|
|
expect(event.defaultPrevented, isFalse);
|
|
});
|
|
|
|
tearDown(() {
|
|
clearForms();
|
|
});
|
|
});
|
|
|
|
group('EngineAutofillForm', () {
|
|
test('validate multi element form', () {
|
|
final List<dynamic> fields = createFieldValues(
|
|
['username', 'password', 'newPassword'],
|
|
['field1', 'field2', '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);
|
|
|
|
expect(autofillForm.formIdentifier, 'field1*field2*field3');
|
|
|
|
final FormElement form = autofillForm.formElement;
|
|
// Note that we also add a submit button. Therefore the form element has
|
|
// 3 child nodes.
|
|
expect(form.childNodes, hasLength(3));
|
|
|
|
final InputElement firstElement = form.childNodes.first;
|
|
// Autofill value is applied to the element.
|
|
expect(firstElement.name,
|
|
BrowserAutofillHints.instance.flutterToEngine('password'));
|
|
expect(firstElement.id,
|
|
BrowserAutofillHints.instance.flutterToEngine('password'));
|
|
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');
|
|
|
|
// For `blink` and `webkit` browser engines the overlay would be hidden.
|
|
if (browserEngine == BrowserEngine.blink ||
|
|
browserEngine == BrowserEngine.webkit) {
|
|
expect(firstElement.classes, contains('transparentTextEditing'));
|
|
} else {
|
|
expect(
|
|
firstElement.classes.any(
|
|
(element) => element.toString() == 'transparentTextEditing'),
|
|
isFalse);
|
|
}
|
|
});
|
|
|
|
test('validate multi element form ids sorted for form id', () {
|
|
final List<dynamic> fields = createFieldValues(
|
|
['username', 'password', 'newPassword'],
|
|
['zzyyxx', 'aabbcc', 'jjkkll']);
|
|
final EngineAutofillForm autofillForm =
|
|
EngineAutofillForm.fromFrameworkMessage(
|
|
createAutofillInfo('username', 'field1'), fields);
|
|
|
|
expect(autofillForm.formIdentifier, 'aabbcc*jjkkll*zzyyxx');
|
|
});
|
|
|
|
test('place and store form', () {
|
|
expect(document.getElementsByTagName('form'), isEmpty);
|
|
|
|
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, form also has the button
|
|
// so in total it shoould have 4 elements.
|
|
final FormElement form = autofillForm.formElement;
|
|
expect(form.childNodes, hasLength(4));
|
|
|
|
final FormElement formOnDom = document.getElementsByTagName('form')[0];
|
|
// Form is attached to the DOM.
|
|
expect(form, equals(formOnDom));
|
|
|
|
autofillForm.storeForm();
|
|
expect(document.getElementsByTagName('form'), isNotEmpty);
|
|
expect(formsOnTheDom, hasLength(1));
|
|
});
|
|
|
|
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;
|
|
// Submit button is added to the form.
|
|
expect(form.childNodes, isNotEmpty);
|
|
final InputElement inputElement = form.childNodes.first;
|
|
expect(inputElement.type, 'submit');
|
|
|
|
// The submit button should have class `submitBtn`.
|
|
expect(inputElement.className, 'submitBtn');
|
|
});
|
|
|
|
test('Return null if no focused element', () {
|
|
final List<dynamic> fields = createFieldValues(['username'], ['field1']);
|
|
final EngineAutofillForm autofillForm =
|
|
EngineAutofillForm.fromFrameworkMessage(null, fields);
|
|
|
|
expect(autofillForm, isNull);
|
|
});
|
|
|
|
tearDown(() {
|
|
clearForms();
|
|
});
|
|
});
|
|
|
|
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,
|
|
BrowserAutofillHints.instance.flutterToEngine(testHint));
|
|
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,
|
|
BrowserAutofillHints.instance.flutterToEngine(testHint));
|
|
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,
|
|
BrowserAutofillHints.instance.flutterToEngine(testPasswordHint));
|
|
expect(testInputElement.type, 'password');
|
|
expect(testInputElement.getAttribute('autocomplete'),
|
|
BrowserAutofillHints.instance.flutterToEngine(testPasswordHint));
|
|
});
|
|
});
|
|
|
|
group('EditingState', () {
|
|
EditingState _editingState;
|
|
|
|
setUp(() {
|
|
editingElement =
|
|
GloballyPositionedTextEditingStrategy(HybridTextEditing());
|
|
editingElement.enable(
|
|
singlelineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
});
|
|
|
|
test('Configure input element from the editing state', () {
|
|
final InputElement input = document.getElementsByTagName('input')[0];
|
|
_editingState =
|
|
EditingState(text: 'Test', baseOffset: 1, extentOffset: 2);
|
|
|
|
_editingState.applyToDomElement(input);
|
|
|
|
expect(input.value, 'Test');
|
|
expect(input.selectionStart, 1);
|
|
expect(input.selectionEnd, 2);
|
|
});
|
|
|
|
test('Configure text area element from the editing state', () {
|
|
cleanTextEditingElement();
|
|
editingElement.enable(
|
|
multilineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
|
|
final TextAreaElement textArea =
|
|
document.getElementsByTagName('textarea')[0];
|
|
_editingState =
|
|
EditingState(text: 'Test', baseOffset: 1, extentOffset: 2);
|
|
|
|
_editingState.applyToDomElement(textArea);
|
|
|
|
expect(textArea.value, 'Test');
|
|
expect(textArea.selectionStart, 1);
|
|
expect(textArea.selectionEnd, 2);
|
|
});
|
|
|
|
test('Get Editing State from input element', () {
|
|
final InputElement input = document.getElementsByTagName('input')[0];
|
|
input.value = 'Test';
|
|
input.selectionStart = 1;
|
|
input.selectionEnd = 2;
|
|
|
|
_editingState = EditingState.fromDomElement(input);
|
|
|
|
expect(_editingState.text, 'Test');
|
|
expect(_editingState.baseOffset, 1);
|
|
expect(_editingState.extentOffset, 2);
|
|
});
|
|
|
|
test('Get Editing State from text area element', () {
|
|
cleanTextEditingElement();
|
|
editingElement.enable(
|
|
multilineConfig,
|
|
onChange: trackEditingState,
|
|
onAction: trackInputAction,
|
|
);
|
|
|
|
final TextAreaElement input =
|
|
document.getElementsByTagName('textarea')[0];
|
|
input.value = 'Test';
|
|
input.selectionStart = 1;
|
|
input.selectionEnd = 2;
|
|
|
|
_editingState = EditingState.fromDomElement(input);
|
|
|
|
expect(_editingState.text, 'Test');
|
|
expect(_editingState.baseOffset, 1);
|
|
expect(_editingState.extentOffset, 2);
|
|
});
|
|
|
|
test('Compare two editing states', () {
|
|
final InputElement input = document.getElementsByTagName('input')[0];
|
|
input.value = 'Test';
|
|
input.selectionStart = 1;
|
|
input.selectionEnd = 2;
|
|
|
|
EditingState editingState1 = EditingState.fromDomElement(input);
|
|
EditingState editingState2 = EditingState.fromDomElement(input);
|
|
|
|
input.setSelectionRange(1, 3);
|
|
|
|
EditingState editingState3 = EditingState.fromDomElement(input);
|
|
|
|
expect(editingState1 == editingState2, true);
|
|
expect(editingState1 != editingState3, true);
|
|
});
|
|
});
|
|
}
|
|
|
|
KeyboardEvent dispatchKeyboardEvent(
|
|
EventTarget target,
|
|
String type, {
|
|
int keyCode,
|
|
}) {
|
|
final Function jsKeyboardEvent = js_util.getProperty(window, 'KeyboardEvent');
|
|
final List<dynamic> eventArgs = <dynamic>[
|
|
type,
|
|
<String, dynamic>{
|
|
'keyCode': keyCode,
|
|
'cancelable': true,
|
|
}
|
|
];
|
|
final KeyboardEvent event =
|
|
js_util.callConstructor(jsKeyboardEvent, js_util.jsify(eventArgs));
|
|
target.dispatchEvent(event);
|
|
|
|
return event;
|
|
}
|
|
|
|
MethodCall configureSetStyleMethodCall(int fontSize, String fontFamily,
|
|
int textAlignIndex, int fontWeightIndex, int textDirectionIndex) {
|
|
return MethodCall('TextInput.setStyle', <String, dynamic>{
|
|
'fontSize': fontSize,
|
|
'fontFamily': fontFamily,
|
|
'textAlignIndex': textAlignIndex,
|
|
'fontWeightIndex': fontWeightIndex,
|
|
'textDirectionIndex': textDirectionIndex,
|
|
});
|
|
}
|
|
|
|
MethodCall configureSetSizeAndTransformMethodCall(
|
|
int width, int height, List<double> transform) {
|
|
return MethodCall('TextInput.setEditableSizeAndTransform', <String, dynamic>{
|
|
'width': width,
|
|
'height': height,
|
|
'transform': transform
|
|
});
|
|
}
|
|
|
|
/// Will disable editing element which will also clean the backup DOM
|
|
/// element from the page.
|
|
void cleanTextEditingElement() {
|
|
if (editingElement != null && editingElement.isEnabled) {
|
|
// Clean up all the DOM elements and event listeners.
|
|
editingElement.disable();
|
|
}
|
|
}
|
|
|
|
void cleanTestFlags() {
|
|
debugBrowserEngineOverride = null;
|
|
debugOperatingSystemOverride = null;
|
|
}
|
|
|
|
void checkInputEditingState(
|
|
InputElement input, String text, int start, int end) {
|
|
expect(document.activeElement, input);
|
|
expect(input.value, text);
|
|
expect(input.selectionStart, start);
|
|
expect(input.selectionEnd, end);
|
|
}
|
|
|
|
/// In case of an exception backup DOM element(s) can still stay on the DOM.
|
|
void clearBackUpDomElementIfExists() {
|
|
List<Node> domElementsToRemove = List<Node>();
|
|
if (document.getElementsByTagName('input').length > 0) {
|
|
domElementsToRemove..addAll(document.getElementsByTagName('input'));
|
|
}
|
|
if (document.getElementsByTagName('textarea').length > 0) {
|
|
domElementsToRemove..addAll(document.getElementsByTagName('textarea'));
|
|
}
|
|
domElementsToRemove.forEach((Node n) => n.remove());
|
|
}
|
|
|
|
void checkTextAreaEditingState(
|
|
TextAreaElement textarea,
|
|
String text,
|
|
int start,
|
|
int end,
|
|
) {
|
|
expect(document.activeElement, textarea);
|
|
expect(textarea.value, text);
|
|
expect(textarea.selectionStart, start);
|
|
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 readOnly = false,
|
|
bool obscureText = false,
|
|
bool autocorrect = true,
|
|
String textCapitalization = 'TextCapitalization.none',
|
|
String inputAction,
|
|
String autofillHint,
|
|
List<String> autofillHintsForFields,
|
|
bool decimal = false,
|
|
}) {
|
|
return <String, dynamic>{
|
|
'inputType': <String, dynamic>{
|
|
'name': 'TextInputType.$inputType',
|
|
if (decimal) 'decimal': true,
|
|
},
|
|
'readOnly': readOnly,
|
|
'obscureText': obscureText,
|
|
'autocorrect': autocorrect,
|
|
'inputAction': inputAction ?? 'TextInputAction.done',
|
|
'textCapitalization': textCapitalization,
|
|
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
|
|
},
|
|
'textCapitalization': 'TextCapitalization.none',
|
|
'autofill': createAutofillInfo(hint, uniqueId)
|
|
};
|
|
|
|
/// In order to not leak test state, clean up the forms from dom if any remains.
|
|
void clearForms() {
|
|
while (document.getElementsByTagName('form').length > 0) {
|
|
document.getElementsByTagName('form').last.remove();
|
|
}
|
|
formsOnTheDom.clear();
|
|
}
|