[web] Use viewId for text editing (flutter/engine#51099)

Handle `viewId` for text fields.

Part of https://github.com/flutter/flutter/issues/137344
This commit is contained in:
Mouad Debbar 2024-03-29 12:50:01 -04:00 committed by GitHub
parent 2b1bbfe870
commit 01ea911472
7 changed files with 419 additions and 66 deletions

View File

@ -95,11 +95,8 @@ final class ViewFocusBinding {
}
int? _viewId(DomElement? element) {
final DomElement? rootElement = element?.closest(DomManager.flutterViewTagName);
if (rootElement == null) {
return null;
}
return _viewManager.viewIdForRootElement(rootElement);
final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager;
return viewManager.findViewForElement(element)?.viewId;
}
void _handleViewCreated(int viewId) {

View File

@ -18,6 +18,8 @@ import '../semantics.dart';
import '../services.dart';
import '../text/paragraph.dart';
import '../util.dart';
import '../view_embedder/flutter_view_manager.dart';
import '../window.dart';
import 'autofill_hint.dart';
import 'composition_aware_mixin.dart';
import 'input_action.dart';
@ -48,12 +50,6 @@ const String transparentTextEditingClass = 'transparentTextEditing';
void _emptyCallback(dynamic _) {}
/// The default [HostNode] that hosts all DOM required for text editing when a11y is not enabled.
@visibleForTesting
// TODO(mdebbar): There could be multiple views with multiple text editing hosts.
// https://github.com/flutter/flutter/issues/137344
DomElement get defaultTextEditingRoot => EnginePlatformDispatcher.instance.implicitView!.dom.textEditingHost;
/// These style attributes are constant throughout the life time of an input
/// element.
///
@ -147,6 +143,37 @@ void _styleAutofillElements(
elementStyle.setProperty('caret-color', 'transparent');
}
void _ensureEditingElementInView(DomElement element, int viewId) {
final bool isAlreadyAppended = element.isConnected ?? false;
if (!isAlreadyAppended) {
// If the element is not already appended to a view, we don't need to move
// it anywhere.
return;
}
final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager;
final EngineFlutterView? currentView = viewManager.findViewForElement(element);
if (currentView == null) {
// For some reason, the input element was in the DOM, but it wasn't part of
// any Flutter view. Should we throw?
return;
}
if (currentView.viewId != viewId) {
_insertEditingElementInView(element, viewId);
}
}
void _insertEditingElementInView(DomElement element, int viewId) {
final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager;
final EngineFlutterView? view = viewManager[viewId];
assert(
view != null,
'Could not find View with id $viewId. This should never happen, please file a bug!',
);
view!.dom.textEditingHost.append(element);
}
/// Form that contains all the fields in the same AutofillGroup.
///
/// An [EngineAutofillForm] will only be constructed when autofill is enabled
@ -154,6 +181,7 @@ void _styleAutofillElements(
/// static method.
class EngineAutofillForm {
EngineAutofillForm({
required this.viewId,
required this.formElement,
this.elements,
this.items,
@ -177,6 +205,9 @@ class EngineAutofillForm {
/// See [formsOnTheDom].
final String formIdentifier;
/// The ID of the view that this form is rendered into.
final int viewId;
/// Creates an [EngineAutofillFrom] from the JSON representation of a Flutter
/// framework `TextInputConfiguration` object.
///
@ -189,6 +220,7 @@ class EngineAutofillForm {
///
/// Returns null if autofill is disabled for the input field.
static EngineAutofillForm? fromFrameworkMessage(
int viewId,
Map<String, dynamic>? focusedElementAutofill,
List<dynamic>? fields,
) {
@ -312,6 +344,7 @@ class EngineAutofillForm {
insertionReferenceNode ??= submitButton;
return EngineAutofillForm(
viewId: viewId,
formElement: formElement,
elements: elements,
items: items,
@ -330,7 +363,7 @@ class EngineAutofillForm {
}
formElement.insertBefore(mainTextEditingElement, insertionReferenceNode);
defaultTextEditingRoot.append(formElement);
_insertEditingElementInView(formElement, viewId);
}
void storeForm() {
@ -944,6 +977,7 @@ class EditingState {
/// This corresponds to Flutter's [TextInputConfiguration].
class InputConfiguration {
InputConfiguration({
required this.viewId,
this.inputType = EngineInputType.text,
this.inputAction = 'TextInputAction.done',
this.obscureText = false,
@ -958,7 +992,8 @@ class InputConfiguration {
InputConfiguration.fromFrameworkMessage(
Map<String, dynamic> flutterInputConfiguration)
: inputType = EngineInputType.fromName(
: viewId = flutterInputConfiguration.tryInt('viewId') ?? kImplicitViewId,
inputType = EngineInputType.fromName(
flutterInputConfiguration.readJson('inputType').readString('name'),
isDecimal: flutterInputConfiguration.readJson('inputType').tryBool('decimal') ?? false,
isMultiline: flutterInputConfiguration.readJson('inputType').tryBool('isMultiline') ?? false,
@ -976,11 +1011,15 @@ class InputConfiguration {
flutterInputConfiguration.readJson('autofill'))
: null,
autofillGroup = EngineAutofillForm.fromFrameworkMessage(
flutterInputConfiguration.tryInt('viewId') ?? kImplicitViewId,
flutterInputConfiguration.tryJson('autofill'),
flutterInputConfiguration.tryList('fields'),
),
enableDeltaModel = flutterInputConfiguration.tryBool('enableDeltaModel') ?? false;
/// The ID of the view that contains the text field.
final int viewId;
/// The type of information being edited in the input control.
final EngineInputType inputType;
@ -1257,7 +1296,7 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
// DOM later, when the first location information arrived.
// Otherwise, on Blink based Desktop browsers, the autofill menu appears
// on top left of the screen.
defaultTextEditingRoot.append(activeDomElement);
_insertEditingElementInView(activeDomElement, inputConfig.viewId);
_appendedToForm = false;
}
@ -1293,6 +1332,9 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
autofill.applyToDomElement(activeDomElement, focusedElement: true);
} else {
activeDomElement.setAttribute('autocomplete', 'off');
// When the new input configuration contains a different view ID, we need
// to move the input element to the new view.
_ensureEditingElementInView(activeDomElement, inputConfiguration.viewId);
}
final String autocorrectValue = config.autocorrect ? 'on' : 'off';
@ -1757,7 +1799,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
if (hasAutofillGroup) {
placeForm();
} else {
defaultTextEditingRoot.append(activeDomElement);
_insertEditingElementInView(activeDomElement, inputConfig.viewId);
}
inputConfig.textCapitalization.setAutocapitalizeAttribute(
activeDomElement);

View File

@ -12,12 +12,15 @@ class FlutterViewManager {
// A map of EngineFlutterViews indexed by their viewId.
final Map<int, EngineFlutterView> _viewData = <int, EngineFlutterView>{};
// A map of (optional) JsFlutterViewOptions, indexed by their viewId.
final Map<int, JsFlutterViewOptions> _jsViewOptions =
<int, JsFlutterViewOptions>{};
// The controller of the [onViewCreated] stream.
final StreamController<int> _onViewCreatedController =
StreamController<int>.broadcast(sync: true);
// The controller of the [onViewDisposed] stream.
final StreamController<int> _onViewDisposedController =
StreamController<int>.broadcast(sync: true);
@ -82,7 +85,7 @@ class FlutterViewManager {
///
/// Returns its [JsFlutterViewOptions] (if any).
JsFlutterViewOptions? unregisterView(int viewId) {
_viewData.remove(viewId); // .dispose();
_viewData.remove(viewId);
final JsFlutterViewOptions? jsViewOptions = _jsViewOptions.remove(viewId);
_onViewDisposedController.add(viewId);
return jsViewOptions;
@ -96,14 +99,13 @@ class FlutterViewManager {
return _jsViewOptions[viewId];
}
/// Returns the [viewId] if [rootElement] corresponds to any of the [views].
int? viewIdForRootElement(DomElement rootElement) {
for(final EngineFlutterView view in views) {
if (view.dom.rootElement == rootElement) {
return view.viewId;
}
}
return null;
EngineFlutterView? findViewForElement(DomElement? element) {
const String viewRootSelector =
'${DomManager.flutterViewTagName}[${GlobalHtmlAttributes.flutterViewIdAttributeName}]';
final DomElement? viewRoot = element?.closest(viewRootSelector);
final String? viewIdAttribute = viewRoot?.getAttribute(GlobalHtmlAttributes.flutterViewIdAttributeName);
final int? viewId = viewIdAttribute == null ? null : int.parse(viewIdAttribute);
return viewId == null ? null : _viewData[viewId];
}
void dispose() {

View File

@ -6,14 +6,13 @@ import 'dart:async';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/browser_detection.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/text_editing/composition_aware_mixin.dart';
import 'package:ui/src/engine/text_editing/text_editing.dart';
import 'package:ui/src/engine.dart';
import '../common/test_initialization.dart';
DomElement get defaultTextEditingRoot =>
EnginePlatformDispatcher.instance.implicitView!.dom.textEditingHost;
void main() {
internalBootstrapBrowserTest(() => testMain);
}
@ -36,7 +35,10 @@ GloballyPositionedTextEditingStrategy _enableEditingStrategy({
}) {
final HybridTextEditing owner = HybridTextEditing();
owner.configuration = InputConfiguration(enableDeltaModel: deltaModel);
owner.configuration = InputConfiguration(
viewId: kImplicitViewId,
enableDeltaModel: deltaModel,
);
final GloballyPositionedTextEditingStrategy editingStrategy =
GloballyPositionedTextEditingStrategy(owner);

View File

@ -15,9 +15,10 @@ import 'package:ui/ui.dart' as ui;
import '../../common/test_initialization.dart';
import 'semantics_tester.dart';
final InputConfiguration singlelineConfig = InputConfiguration();
final InputConfiguration singlelineConfig = InputConfiguration(viewId: kImplicitViewId);
final InputConfiguration multilineConfig = InputConfiguration(
viewId: kImplicitViewId,
inputType: EngineInputType.multiline,
inputAction: 'TextInputAction.newline',
);

View File

@ -8,16 +8,7 @@ import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/browser_detection.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/raw_keyboard.dart';
import 'package:ui/src/engine/services.dart';
import 'package:ui/src/engine/text_editing/autofill_hint.dart';
import 'package:ui/src/engine/text_editing/input_type.dart';
import 'package:ui/src/engine/text_editing/text_editing.dart';
import 'package:ui/src/engine/util.dart';
import 'package:ui/src/engine/vector_math.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../common/spy.dart';
@ -28,6 +19,11 @@ const int _kReturnKeyCode = 13;
const MethodCodec codec = JSONMethodCodec();
EnginePlatformDispatcher get dispatcher => EnginePlatformDispatcher.instance;
DomElement get defaultTextEditingRoot =>
dispatcher.implicitView!.dom.textEditingHost;
/// Add unit tests for [FirefoxTextEditingStrategy].
// TODO(mdebbar): https://github.com/flutter/flutter/issues/46891
@ -36,11 +32,14 @@ EditingState? lastEditingState;
TextEditingDeltaState? editingDeltaState;
String? lastInputAction;
final InputConfiguration singlelineConfig = InputConfiguration();
final InputConfiguration singlelineConfig = InputConfiguration(
viewId: kImplicitViewId,
);
final Map<String, dynamic> flutterSinglelineConfig =
createFlutterConfig('text');
final InputConfiguration multilineConfig = InputConfiguration(
viewId: kImplicitViewId,
inputType: EngineInputType.multiline,
inputAction: 'TextInputAction.newline',
);
@ -129,8 +128,38 @@ Future<void> testMain() async {
domDocument.body);
});
test('inserts element in the correct view', () {
final DomElement host = createDomElement('div');
domDocument.body!.append(host);
final EngineFlutterView view = EngineFlutterView(dispatcher, host);
dispatcher.viewManager.registerView(view);
final DomElement textEditingHost = view.dom.textEditingHost;
expect(domDocument.getElementsByTagName('input'), hasLength(0));
expect(textEditingHost.getElementsByTagName('input'), hasLength(0));
final InputConfiguration config = InputConfiguration(viewId: view.viewId);
editingStrategy!.enable(
config,
onChange: trackEditingState,
onAction: trackInputAction,
);
final DomElement input = editingStrategy!.domElement!;
// Input is appended to the right view.
expect(textEditingHost.contains(input), isTrue);
// Cleanup.
editingStrategy!.disable();
expect(textEditingHost.querySelectorAll('input'), hasLength(0));
dispatcher.viewManager.unregisterView(view.viewId);
view.dispose();
host.remove();
});
test('Respects read-only config', () {
final InputConfiguration config = InputConfiguration(
viewId: kImplicitViewId,
readOnly: true,
);
editingStrategy!.enable(
@ -148,6 +177,7 @@ Future<void> testMain() async {
test('Knows how to create password fields', () {
final InputConfiguration config = InputConfiguration(
viewId: kImplicitViewId,
obscureText: true,
);
editingStrategy!.enable(
@ -165,6 +195,7 @@ Future<void> testMain() async {
test('Knows how to create non-default text actions', () {
final InputConfiguration config = InputConfiguration(
viewId: kImplicitViewId,
inputAction: 'TextInputAction.send'
);
editingStrategy!.enable(
@ -186,6 +217,7 @@ Future<void> testMain() async {
test('Knows to turn autocorrect off', () {
final InputConfiguration config = InputConfiguration(
viewId: kImplicitViewId,
autocorrect: false,
);
editingStrategy!.enable(
@ -202,7 +234,7 @@ Future<void> testMain() async {
});
test('Knows to turn autocorrect on', () {
final InputConfiguration config = InputConfiguration();
final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId);
editingStrategy!.enable(
config,
onChange: trackEditingState,
@ -217,7 +249,7 @@ Future<void> testMain() async {
});
test('Knows to turn autofill off', () {
final InputConfiguration config = InputConfiguration();
final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId);
editingStrategy!.enable(
config,
onChange: trackEditingState,
@ -352,7 +384,7 @@ Future<void> testMain() async {
});
test('Triggers input action', () {
final InputConfiguration config = InputConfiguration();
final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId);
editingStrategy!.enable(
config,
onChange: trackEditingState,
@ -371,10 +403,10 @@ Future<void> testMain() async {
});
test('handling keyboard event prevents triggering input action', () {
final ui.PlatformMessageCallback? savedCallback = ui.PlatformDispatcher.instance.onPlatformMessage;
final ui.PlatformMessageCallback? savedCallback = dispatcher.onPlatformMessage;
bool markTextEventHandled = false;
ui.PlatformDispatcher.instance.onPlatformMessage = (String channel, ByteData? data,
dispatcher.onPlatformMessage = (String channel, ByteData? data,
ui.PlatformMessageResponseCallback? callback) {
final ByteData response = const JSONMessageCodec()
.encodeMessage(<String, dynamic>{'handled': markTextEventHandled})!;
@ -382,7 +414,7 @@ Future<void> testMain() async {
};
RawKeyboard.initialize();
final InputConfiguration config = InputConfiguration();
final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId);
editingStrategy!.enable(
config,
onChange: trackEditingState,
@ -412,12 +444,13 @@ Future<void> testMain() async {
// Input action received.
expect(lastInputAction, 'TextInputAction.done');
ui.PlatformDispatcher.instance.onPlatformMessage = savedCallback;
dispatcher.onPlatformMessage = savedCallback;
RawKeyboard.instance?.dispose();
});
test('Triggers input action in multi-line mode', () {
final InputConfiguration config = InputConfiguration(
viewId: kImplicitViewId,
inputType: EngineInputType.multiline,
);
editingStrategy!.enable(
@ -443,6 +476,7 @@ Future<void> testMain() async {
test('Triggers input action in multiline-none mode', () {
final InputConfiguration config = InputConfiguration(
viewId: kImplicitViewId,
inputType: EngineInputType.multilineNone,
);
editingStrategy!.enable(
@ -468,7 +502,7 @@ Future<void> testMain() async {
test('Triggers input action and prevent new line key event for single line field', () {
// Regression test for https://github.com/flutter/flutter/issues/113559
final InputConfiguration config = InputConfiguration();
final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId);
editingStrategy!.enable(
config,
onChange: trackEditingState,
@ -590,16 +624,24 @@ Future<void> testMain() async {
/// Returns the `clientId` used in the platform message.
int showKeyboard({
required String inputType,
int? viewId,
String? inputAction,
bool decimal = false,
bool isMultiline = false,
bool autofillEnabled = true,
}) {
final MethodCall setClient = MethodCall(
'TextInput.setClient',
<dynamic>[
++clientId,
createFlutterConfig(inputType,
inputAction: inputAction, decimal: decimal, isMultiline: isMultiline),
createFlutterConfig(
inputType,
viewId: viewId,
inputAction: inputAction,
decimal: decimal,
isMultiline: isMultiline,
autofillEnabled: autofillEnabled,
),
],
);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
@ -2481,6 +2523,186 @@ Future<void> testMain() async {
expect(event.defaultPrevented, isFalse);
});
test('inserts element in the correct view', () async {
final DomElement host = createDomElement('div');
domDocument.body!.append(host);
final EngineFlutterView view = EngineFlutterView(dispatcher, host);
dispatcher.viewManager.registerView(view);
textEditing = HybridTextEditing();
showKeyboard(inputType: 'text', viewId: view.viewId);
// The Safari strategy doesn't insert the input element into the DOM until
// it has received the geometry information.
final List<double> transform = Matrix4.identity().storage.toList();
final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(10, 10, transform);
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
await waitForDesktopSafariFocus();
final DomElement input = textEditing!.strategy.domElement!;
// Input is appended to the right view.
expect(view.dom.textEditingHost.contains(input), isTrue);
// Cleanup.
hideKeyboard();
dispatcher.viewManager.unregisterView(view.viewId);
view.dispose();
host.remove();
});
test('moves element to correct view', () {
final DomElement host1 = createDomElement('div');
domDocument.body!.append(host1);
final EngineFlutterView view1 = EngineFlutterView(dispatcher, host1);
dispatcher.viewManager.registerView(view1);
final DomElement host2 = createDomElement('div');
domDocument.body!.append(host2);
final EngineFlutterView view2 = EngineFlutterView(dispatcher, host2);
dispatcher.viewManager.registerView(view2);
textEditing = HybridTextEditing();
showKeyboard(inputType: 'text', viewId: view1.viewId, autofillEnabled: false);
final DomElement input = textEditing!.strategy.domElement!;
// Input is appended to view1.
expect(view1.dom.textEditingHost.contains(input), isTrue);
sendFrameworkMessage(codec.encodeMethodCall(MethodCall(
'TextInput.updateConfig',
createFlutterConfig('text', viewId: view2.viewId, autofillEnabled: false),
)));
// The input element is the same (no new element was created), but it has
// moved to view2.
expect(textEditing!.strategy.domElement, input);
expect(view2.dom.textEditingHost.contains(input), isTrue);
// Cleanup.
hideKeyboard();
dispatcher.viewManager.unregisterView(view1.viewId);
view1.dispose();
dispatcher.viewManager.unregisterView(view2.viewId);
view2.dispose();
host1.remove();
host2.remove();
});
test('places autofill form in the correct view', () async {
final DomElement host = createDomElement('div');
domDocument.body!.append(host);
final EngineFlutterView view = EngineFlutterView(dispatcher, host);
dispatcher.viewManager.registerView(view);
textEditing = HybridTextEditing();
// Create a configuration with an AutofillGroup of three text fields.
final Map<String, dynamic> flutterMultiAutofillElementConfig =
createFlutterConfig(
'text',
viewId: view.viewId,
autofillHint: 'username',
autofillHintsForFields: <String>['username', 'email', 'name'],
);
final MethodCall setClient = MethodCall(
'TextInput.setClient',
<dynamic>[123, flutterMultiAutofillElementConfig],
);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
// The Safari strategy doesn't insert the input element into the DOM until
// it has received the geometry information.
final List<double> transform = Matrix4.identity().storage.toList();
final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(10, 10, transform);
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
await waitForDesktopSafariFocus();
final DomElement input = textEditing!.strategy.domElement!;
final DomElement form = textEditing!.configuration!.autofillGroup!.formElement;
// Input and form are appended to the right view.
expect(view.dom.textEditingHost.contains(input), isTrue);
expect(view.dom.textEditingHost.contains(form), isTrue);
// Cleanup.
hideKeyboard();
dispatcher.viewManager.unregisterView(view.viewId);
view.dispose();
host.remove();
});
test('moves autofill form to the correct view', () async {
final DomElement host1 = createDomElement('div');
domDocument.body!.append(host1);
final EngineFlutterView view1 = EngineFlutterView(dispatcher, host1);
dispatcher.viewManager.registerView(view1);
final DomElement host2 = createDomElement('div');
domDocument.body!.append(host2);
final EngineFlutterView view2 = EngineFlutterView(dispatcher, host2);
dispatcher.viewManager.registerView(view2);
textEditing = HybridTextEditing();
// Create a configuration with an AutofillGroup of three text fields.
final Map<String, dynamic> autofillConfig1 = createFlutterConfig(
'text',
viewId: view1.viewId,
autofillHint: 'username',
autofillHintsForFields: <String>['username', 'email', 'name'],
);
final MethodCall setClient = MethodCall(
'TextInput.setClient',
<dynamic>[123, autofillConfig1],
);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
await waitForDesktopSafariFocus();
final DomElement input = textEditing!.strategy.domElement!;
final DomElement form = textEditing!.configuration!.autofillGroup!.formElement;
// Input and form are appended to view1.
expect(view1.dom.textEditingHost.contains(input), isTrue);
expect(view1.dom.textEditingHost.contains(form), isTrue);
// Move the input and form to view2.
final Map<String, dynamic> autofillConfig2 = createFlutterConfig(
'text',
viewId: view2.viewId,
autofillHint: 'username',
autofillHintsForFields: <String>['username', 'email', 'name'],
);
sendFrameworkMessage(codec.encodeMethodCall(MethodCall(
'TextInput.updateConfig',
autofillConfig2,
)));
// Input and form are in view2.
expect(view2.dom.textEditingHost.contains(input), isTrue);
expect(view2.dom.textEditingHost.contains(form), isTrue);
// Cleanup.
hideKeyboard();
dispatcher.viewManager.unregisterView(view1.viewId);
view1.dispose();
dispatcher.viewManager.unregisterView(view2.viewId);
view2.dispose();
host1.remove();
host2.remove();
// TODO(mdebbar): Autofill forms don't get updated in the current system.
// https://github.com/flutter/flutter/issues/145101
}, skip: true);
tearDown(() {
clearForms();
});
@ -2493,7 +2715,10 @@ Future<void> testMain() async {
<String>['field1', 'field2', 'field3']);
final EngineAutofillForm autofillForm =
EngineAutofillForm.fromFrameworkMessage(
createAutofillInfo('username', 'field1'), fields)!;
kImplicitViewId,
createAutofillInfo('username', 'field1'),
fields,
)!;
// Number of elements if number of fields sent to the constructor minus
// one (for the focused text element).
@ -2550,7 +2775,10 @@ Future<void> testMain() async {
<String>['zzyyxx', 'aabbcc', 'jjkkll']);
final EngineAutofillForm autofillForm =
EngineAutofillForm.fromFrameworkMessage(
createAutofillInfo('username', 'field1'), fields)!;
kImplicitViewId,
createAutofillInfo('username', 'field1'),
fields,
)!;
expect(autofillForm.formIdentifier, 'aabbcc*jjkkll*zzyyxx');
});
@ -2563,7 +2791,10 @@ Future<void> testMain() async {
<String>['field1', 'fields2', 'field3']);
final EngineAutofillForm autofillForm =
EngineAutofillForm.fromFrameworkMessage(
createAutofillInfo('username', 'field1'), fields)!;
kImplicitViewId,
createAutofillInfo('username', 'field1'),
fields,
)!;
final DomHTMLInputElement testInputElement = createDomHTMLInputElement();
autofillForm.placeForm(testInputElement);
@ -2590,7 +2821,10 @@ Future<void> testMain() async {
);
final EngineAutofillForm autofillForm =
EngineAutofillForm.fromFrameworkMessage(
createAutofillInfo('username', 'field1'), fields)!;
kImplicitViewId,
createAutofillInfo('username', 'field1'),
fields,
)!;
// The focused element is the only field. Form should be empty after
// the initialization (focus element is appended later).
@ -2615,7 +2849,7 @@ Future<void> testMain() async {
<String>['field1'],
);
final EngineAutofillForm? autofillForm =
EngineAutofillForm.fromFrameworkMessage(null, fields);
EngineAutofillForm.fromFrameworkMessage(kImplicitViewId, null, fields);
expect(autofillForm, isNull);
});
@ -2632,7 +2866,10 @@ Future<void> testMain() async {
]);
final EngineAutofillForm autofillForm =
EngineAutofillForm.fromFrameworkMessage(
createAutofillInfo('email', 'field1'), fields)!;
kImplicitViewId,
createAutofillInfo('email', 'field1'),
fields,
)!;
expect(autofillForm.elements, hasLength(2));
@ -2675,7 +2912,10 @@ Future<void> testMain() async {
]);
final EngineAutofillForm autofillForm =
EngineAutofillForm.fromFrameworkMessage(
createAutofillInfo('email', 'field1'), fields)!;
kImplicitViewId,
createAutofillInfo('email', 'field1'),
fields,
)!;
final List<DomHTMLInputElement> formChildNodes =
autofillForm.formElement.childNodes.toList()
as List<DomHTMLInputElement>;
@ -2707,7 +2947,10 @@ Future<void> testMain() async {
]);
final EngineAutofillForm autofillForm =
EngineAutofillForm.fromFrameworkMessage(
createAutofillInfo('email', 'field1'), fields)!;
kImplicitViewId,
createAutofillInfo('email', 'field1'),
fields,
)!;
final List<DomHTMLInputElement> formChildNodes =
autofillForm.formElement.childNodes.toList()
as List<DomHTMLInputElement>;
@ -2738,7 +2981,10 @@ Future<void> testMain() async {
]);
final EngineAutofillForm autofillForm =
EngineAutofillForm.fromFrameworkMessage(
createAutofillInfo('email', 'field1'), fields)!;
kImplicitViewId,
createAutofillInfo('email', 'field1'),
fields,
)!;
final DomHTMLInputElement testInputElement = createDomHTMLInputElement();
testInputElement.name = 'email';
@ -3378,6 +3624,7 @@ void checkTextAreaEditingState(
/// simplicity.
Map<String, dynamic> createFlutterConfig(
String inputType, {
int? viewId,
bool readOnly = false,
bool obscureText = false,
bool autocorrect = true,
@ -3397,6 +3644,7 @@ Map<String, dynamic> createFlutterConfig(
if (decimal) 'decimal': true,
if (isMultiline) 'isMultiline': true,
},
if (viewId != null) 'viewId': viewId,
'readOnly': readOnly,
'obscureText': obscureText,
'autocorrect': autocorrect,

View File

@ -102,13 +102,74 @@ Future<void> doTests() async {
});
});
group('viewIdForRootElement', () {
test('works', () {
final EngineFlutterView view = EngineFlutterView(platformDispatcher, createDomElement('div'));
final int viewId = view.viewId;
group('findViewForElement', () {
test('finds view for root and descendant elements', () {
final DomElement host = createDomElement('div');
final EngineFlutterView view = EngineFlutterView(platformDispatcher, host);
viewManager.registerView(view);
expect(viewManager.viewIdForRootElement(view.dom.rootElement), viewId);
final DomElement rootElement = view.dom.rootElement;
final DomElement child1 = createDomElement('div');
final DomElement child2 = createDomElement('div');
final DomElement child3 = createDomElement('div');
rootElement.append(child1);
rootElement.append(child2);
child2.append(child3);
expect(viewManager.findViewForElement(rootElement), view);
expect(viewManager.findViewForElement(child1), view);
expect(viewManager.findViewForElement(child2), view);
expect(viewManager.findViewForElement(child3), view);
});
test('returns null for host element', () {
final DomElement host = createDomElement('div');
final EngineFlutterView view = EngineFlutterView(platformDispatcher, host);
viewManager.registerView(view);
expect(viewManager.findViewForElement(host), isNull);
});
test("returns null for elements that don't belong to any view", () {
final DomElement host = createDomElement('div');
final EngineFlutterView view = EngineFlutterView(platformDispatcher, host);
viewManager.registerView(view);
final DomElement disconnectedElement = createDomElement('div');
final DomElement childOfBody = createDomElement('div');
domDocument.body!.append(childOfBody);
expect(viewManager.findViewForElement(disconnectedElement), isNull);
expect(viewManager.findViewForElement(childOfBody), isNull);
expect(viewManager.findViewForElement(domDocument.body), isNull);
});
test('does not recognize elements from unregistered views', () {
final DomElement host = createDomElement('div');
final EngineFlutterView view = EngineFlutterView(platformDispatcher, host);
viewManager.registerView(view);
final DomElement rootElement = view.dom.rootElement;
final DomElement child1 = createDomElement('div');
final DomElement child2 = createDomElement('div');
final DomElement child3 = createDomElement('div');
rootElement.append(child1);
rootElement.append(child2);
child2.append(child3);
expect(viewManager.findViewForElement(rootElement), view);
expect(viewManager.findViewForElement(child1), view);
expect(viewManager.findViewForElement(child2), view);
expect(viewManager.findViewForElement(child3), view);
viewManager.unregisterView(view.viewId);
expect(viewManager.findViewForElement(rootElement), isNull);
expect(viewManager.findViewForElement(child1), isNull);
expect(viewManager.findViewForElement(child2), isNull);
expect(viewManager.findViewForElement(child3), isNull);
});
});
});