From df152f1450b5923178eaed53cd838ca36877d4b0 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Thu, 8 Feb 2024 13:40:26 -0500 Subject: [PATCH 1/7] [web] Use viewId for text editing --- .../view_focus_binding.dart | 9 +-- .../src/engine/text_editing/text_editing.dart | 78 ++++++++++++++++--- lib/web_ui/lib/src/engine/util.dart | 11 +++ .../view_embedder/flutter_view_manager.dart | 25 +++++- lib/web_ui/test/engine/composition_test.dart | 14 ++-- .../engine/semantics/text_field_test.dart | 3 +- lib/web_ui/test/engine/text_editing_test.dart | 77 ++++++++++++------ .../flutter_view_manager_test.dart | 71 +++++++++++++++++ 8 files changed, 237 insertions(+), 51 deletions(-) diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart b/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart index db0cd066da42b..d20d749aa3134 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart @@ -90,13 +90,8 @@ final class ViewFocusBinding { } static int? _viewId(DomElement? element) { - final DomElement? viewElement = element?.closest( - DomManager.flutterViewTagName, - ); - final String? viewIdAttribute = viewElement?.getAttribute( - GlobalHtmlAttributes.flutterViewIdAttributeName, - ); - return viewIdAttribute == null ? null : int.tryParse(viewIdAttribute); + final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager; + return viewManager.findViewForElement(element)?.viewId; } static const String _focusin = 'focusin'; diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 813885329be1d..d46d085aa440e 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -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,39 @@ 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]; + if (view == null) { + // `viewId` points to a non-existent view, is this even possible? Should we + // throw? + return; + } + + 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,12 +183,13 @@ void _styleAutofillElements( /// static method. class EngineAutofillForm { EngineAutofillForm({ + required int viewId, required this.formElement, this.elements, this.items, this.formIdentifier = '', this.insertionReferenceNode, - }); + }) : _viewId = viewId; final DomHTMLFormElement formElement; @@ -177,6 +207,16 @@ class EngineAutofillForm { /// See [formsOnTheDom]. final String formIdentifier; + int _viewId; + int get viewId => _viewId; + set viewId(int value) { + if (_viewId == value) { + return; + } + _viewId = value; + _ensureEditingElementInView(formElement, _viewId); + } + /// Creates an [EngineAutofillFrom] from the JSON representation of a Flutter /// framework `TextInputConfiguration` object. /// @@ -189,6 +229,7 @@ class EngineAutofillForm { /// /// Returns null if autofill is disabled for the input field. static EngineAutofillForm? fromFrameworkMessage( + int viewId, Map? focusedElementAutofill, List? fields, ) { @@ -312,6 +353,7 @@ class EngineAutofillForm { insertionReferenceNode ??= submitButton; return EngineAutofillForm( + viewId: viewId, formElement: formElement, elements: elements, items: items, @@ -330,7 +372,7 @@ class EngineAutofillForm { } formElement.insertBefore(mainTextEditingElement, insertionReferenceNode); - defaultTextEditingRoot.append(formElement); + _insertEditingElementInView(formElement, viewId); } void storeForm() { @@ -944,6 +986,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 +1001,8 @@ class InputConfiguration { InputConfiguration.fromFrameworkMessage( Map 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 +1020,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; @@ -1238,6 +1286,8 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements DomHTMLFormElement? get focusedFormElement => inputConfiguration.autofillGroup?.formElement; + FlutterViewManager get viewManager => EnginePlatformDispatcher.instance.viewManager; + @override void initializeTextEditing( InputConfiguration inputConfig, { @@ -1257,7 +1307,8 @@ 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); + final DomElement textEditingHost = viewManager[inputConfig.viewId]!.dom.textEditingHost; + textEditingHost.append(activeDomElement); _appendedToForm = false; } @@ -1291,8 +1342,12 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements final AutofillInfo? autofill = config.autofill; if (autofill != null) { autofill.applyToDomElement(activeDomElement, focusedElement: true); + config.autofillGroup!.viewId = config.viewId; } 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'; @@ -1748,7 +1803,8 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy { if (hasAutofillGroup) { placeForm(); } else { - defaultTextEditingRoot.append(activeDomElement); + final DomElement textEditingHost = viewManager[inputConfig.viewId]!.dom.textEditingHost; + textEditingHost.append(activeDomElement); } inputConfig.textCapitalization.setAutocapitalizeAttribute( activeDomElement); diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index 5f54070c155f8..6edc1b269eb19 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -16,6 +16,8 @@ import 'dom.dart'; import 'safe_browser_api.dart'; import 'services.dart'; import 'vector_math.dart'; +import 'view_embedder/dom_manager.dart'; +import 'view_embedder/global_html_attributes.dart'; /// Generic callback signature, used by [_futurize]. typedef Callback = void Function(T result); @@ -649,6 +651,15 @@ int? tryViewId(Object? arguments) { return null; } +int? findParentViewId(DomElement element) { + DomElement? current = element; + while (current != null && element.tagName.toLowerCase() != DomManager.flutterViewTagName) { + current = element.parent; + } + final String? viewIdAttribute = current?.getAttribute(GlobalHtmlAttributes.flutterViewIdAttributeName); + return viewIdAttribute == null ? null : int.parse(viewIdAttribute); +} + /// Prints a list of bytes in hex format. /// /// Bytes are separated by one space and are padded on the left to always show diff --git a/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart b/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart index e93956f0325a1..7553841bc0331 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart @@ -12,12 +12,19 @@ class FlutterViewManager { // A map of EngineFlutterViews indexed by their viewId. final Map _viewData = {}; + // A map of (optional) JsFlutterViewOptions, indexed by their viewId. final Map _jsViewOptions = {}; + + // A map of root elements to their corresponding EngineFlutterView. + final Map _elementToView = + {}; + // The controller of the [onViewCreated] stream. final StreamController _onViewCreatedController = StreamController.broadcast(sync: true); + // The controller of the [onViewDisposed] stream. final StreamController _onViewDisposedController = StreamController.broadcast(sync: true); @@ -60,6 +67,7 @@ class FlutterViewManager { // Store the view, and the jsViewOptions, if any... _viewData[viewId] = view; + _elementToView[view.dom.rootElement] = view; if (jsViewOptions != null) { _jsViewOptions[viewId] = jsViewOptions; } @@ -82,7 +90,10 @@ class FlutterViewManager { /// /// Returns its [JsFlutterViewOptions] (if any). JsFlutterViewOptions? unregisterView(int viewId) { - _viewData.remove(viewId); // .dispose(); + final EngineFlutterView? unregisteredView = _viewData.remove(viewId); + if (unregisteredView != null) { + _elementToView.remove(unregisteredView.dom.rootElement); + } final JsFlutterViewOptions? jsViewOptions = _jsViewOptions.remove(viewId); _onViewDisposedController.add(viewId); return jsViewOptions; @@ -96,6 +107,18 @@ class FlutterViewManager { return _jsViewOptions[viewId]; } + EngineFlutterView? findViewForElement(DomElement? element) { + DomElement? current = element; + while (current != null) { + final EngineFlutterView? view = _elementToView[current]; + if (view != null) { + return view; + } + current = current.parent; + } + return null; + } + void dispose() { // We need to call `toList()` in order to avoid concurrent modification // inside the loop. diff --git a/lib/web_ui/test/engine/composition_test.dart b/lib/web_ui/test/engine/composition_test.dart index 6e743139bd47d..75551745b3d56 100644 --- a/lib/web_ui/test/engine/composition_test.dart +++ b/lib/web_ui/test/engine/composition_test.dart @@ -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); diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart index 60e4cbaa31487..52a41b1ae197d 100644 --- a/lib/web_ui/test/engine/semantics/text_field_test.dart +++ b/lib/web_ui/test/engine/semantics/text_field_test.dart @@ -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', ); diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index 09a99a242d964..86967521d9a76 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -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,9 @@ const int _kReturnKeyCode = 13; const MethodCodec codec = JSONMethodCodec(); +DomElement get defaultTextEditingRoot => + EnginePlatformDispatcher.instance.implicitView!.dom.textEditingHost; + /// Add unit tests for [FirefoxTextEditingStrategy]. // TODO(mdebbar): https://github.com/flutter/flutter/issues/46891 @@ -36,11 +30,14 @@ EditingState? lastEditingState; TextEditingDeltaState? editingDeltaState; String? lastInputAction; -final InputConfiguration singlelineConfig = InputConfiguration(); +final InputConfiguration singlelineConfig = InputConfiguration( + viewId: kImplicitViewId, +); final Map flutterSinglelineConfig = createFlutterConfig('text'); final InputConfiguration multilineConfig = InputConfiguration( + viewId: kImplicitViewId, inputType: EngineInputType.multiline, inputAction: 'TextInputAction.newline', ); @@ -131,6 +128,7 @@ Future testMain() async { test('Respects read-only config', () { final InputConfiguration config = InputConfiguration( + viewId: kImplicitViewId, readOnly: true, ); editingStrategy!.enable( @@ -148,6 +146,7 @@ Future testMain() async { test('Knows how to create password fields', () { final InputConfiguration config = InputConfiguration( + viewId: kImplicitViewId, obscureText: true, ); editingStrategy!.enable( @@ -165,6 +164,7 @@ Future testMain() async { test('Knows how to create non-default text actions', () { final InputConfiguration config = InputConfiguration( + viewId: kImplicitViewId, inputAction: 'TextInputAction.send' ); editingStrategy!.enable( @@ -186,6 +186,7 @@ Future testMain() async { test('Knows to turn autocorrect off', () { final InputConfiguration config = InputConfiguration( + viewId: kImplicitViewId, autocorrect: false, ); editingStrategy!.enable( @@ -202,7 +203,7 @@ Future 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 +218,7 @@ Future 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 +353,7 @@ Future testMain() async { }); test('Triggers input action', () { - final InputConfiguration config = InputConfiguration(); + final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId); editingStrategy!.enable( config, onChange: trackEditingState, @@ -382,7 +383,7 @@ Future testMain() async { }; RawKeyboard.initialize(); - final InputConfiguration config = InputConfiguration(); + final InputConfiguration config = InputConfiguration(viewId: kImplicitViewId); editingStrategy!.enable( config, onChange: trackEditingState, @@ -418,6 +419,7 @@ Future testMain() async { test('Triggers input action in multi-line mode', () { final InputConfiguration config = InputConfiguration( + viewId: kImplicitViewId, inputType: EngineInputType.multiline, ); editingStrategy!.enable( @@ -443,6 +445,7 @@ Future testMain() async { test('Triggers input action in multiline-none mode', () { final InputConfiguration config = InputConfiguration( + viewId: kImplicitViewId, inputType: EngineInputType.multilineNone, ); editingStrategy!.enable( @@ -468,7 +471,7 @@ Future 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, @@ -2435,7 +2438,10 @@ Future testMain() async { ['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). @@ -2492,7 +2498,10 @@ Future testMain() async { ['zzyyxx', 'aabbcc', 'jjkkll']); final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage( - createAutofillInfo('username', 'field1'), fields)!; + kImplicitViewId, + createAutofillInfo('username', 'field1'), + fields, + )!; expect(autofillForm.formIdentifier, 'aabbcc*jjkkll*zzyyxx'); }); @@ -2505,7 +2514,10 @@ Future testMain() async { ['field1', 'fields2', 'field3']); final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage( - createAutofillInfo('username', 'field1'), fields)!; + kImplicitViewId, + createAutofillInfo('username', 'field1'), + fields, + )!; final DomHTMLInputElement testInputElement = createDomHTMLInputElement(); autofillForm.placeForm(testInputElement); @@ -2532,7 +2544,10 @@ Future 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). @@ -2557,7 +2572,7 @@ Future testMain() async { ['field1'], ); final EngineAutofillForm? autofillForm = - EngineAutofillForm.fromFrameworkMessage(null, fields); + EngineAutofillForm.fromFrameworkMessage(kImplicitViewId, null, fields); expect(autofillForm, isNull); }); @@ -2574,7 +2589,10 @@ Future testMain() async { ]); final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage( - createAutofillInfo('email', 'field1'), fields)!; + kImplicitViewId, + createAutofillInfo('email', 'field1'), + fields, + )!; expect(autofillForm.elements, hasLength(2)); @@ -2617,7 +2635,10 @@ Future testMain() async { ]); final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage( - createAutofillInfo('email', 'field1'), fields)!; + kImplicitViewId, + createAutofillInfo('email', 'field1'), + fields, + )!; final List formChildNodes = autofillForm.formElement.childNodes.toList() as List; @@ -2649,7 +2670,10 @@ Future testMain() async { ]); final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage( - createAutofillInfo('email', 'field1'), fields)!; + kImplicitViewId, + createAutofillInfo('email', 'field1'), + fields, + )!; final List formChildNodes = autofillForm.formElement.childNodes.toList() as List; @@ -2680,7 +2704,10 @@ Future testMain() async { ]); final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage( - createAutofillInfo('email', 'field1'), fields)!; + kImplicitViewId, + createAutofillInfo('email', 'field1'), + fields, + )!; final DomHTMLInputElement testInputElement = createDomHTMLInputElement(); testInputElement.name = 'email'; diff --git a/lib/web_ui/test/engine/view_embedder/flutter_view_manager_test.dart b/lib/web_ui/test/engine/view_embedder/flutter_view_manager_test.dart index 04fb618e9dc6c..3ff2cd027e1df 100644 --- a/lib/web_ui/test/engine/view_embedder/flutter_view_manager_test.dart +++ b/lib/web_ui/test/engine/view_embedder/flutter_view_manager_test.dart @@ -101,5 +101,76 @@ Future doTests() async { reason: 'Should fire dispose event for view'); }); }); + + group('findViewForElement', () { + test('finds view for root and descendant elements', () { + 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); + }); + + 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); + }); + }); }); } From d3473de6259cc23a9917674869888c80781a1be9 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Thu, 7 Mar 2024 17:10:43 -0500 Subject: [PATCH 2/7] tests for non-autofill cases --- lib/web_ui/test/engine/text_editing_test.dart | 112 +++++++++++++++++- 1 file changed, 106 insertions(+), 6 deletions(-) diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index 86967521d9a76..cfd6b8cc7995b 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -19,8 +19,10 @@ const int _kReturnKeyCode = 13; const MethodCodec codec = JSONMethodCodec(); +EnginePlatformDispatcher get dispatcher => EnginePlatformDispatcher.instance; + DomElement get defaultTextEditingRoot => - EnginePlatformDispatcher.instance.implicitView!.dom.textEditingHost; + dispatcher.implicitView!.dom.textEditingHost; /// Add unit tests for [FirefoxTextEditingStrategy]. // TODO(mdebbar): https://github.com/flutter/flutter/issues/46891 @@ -126,6 +128,35 @@ Future 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, @@ -372,10 +403,10 @@ Future 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({'handled': markTextEventHandled})!; @@ -413,7 +444,7 @@ Future testMain() async { // Input action received. expect(lastInputAction, 'TextInputAction.done'); - ui.PlatformDispatcher.instance.onPlatformMessage = savedCallback; + dispatcher.onPlatformMessage = savedCallback; RawKeyboard.instance?.dispose(); }); @@ -535,16 +566,24 @@ Future 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', [ ++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)); @@ -2426,6 +2465,65 @@ Future testMain() async { expect(event.defaultPrevented, isFalse); }); + 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); + + textEditing = HybridTextEditing(); + showKeyboard(inputType: 'text', viewId: view.viewId); + + 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('move element to correct view', () { + final DomElement host1 = createDomElement('div'); + final DomElement host2 = createDomElement('div'); + domDocument.body!.append(host1); + domDocument.body!.append(host2); + final EngineFlutterView view1 = EngineFlutterView(dispatcher, host1); + dispatcher.viewManager.registerView(view1); + 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(); + }); + tearDown(() { clearForms(); }); @@ -3347,6 +3445,7 @@ void checkTextAreaEditingState( /// simplicity. Map createFlutterConfig( String inputType, { + int? viewId, bool readOnly = false, bool obscureText = false, bool autocorrect = true, @@ -3366,6 +3465,7 @@ Map createFlutterConfig( if (decimal) 'decimal': true, if (isMultiline) 'isMultiline': true, }, + if (viewId != null) 'viewId': viewId, 'readOnly': readOnly, 'obscureText': obscureText, 'autocorrect': autocorrect, From 5710d60d45ef3f354aee7373c845bc5dc9f6029a Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Wed, 13 Mar 2024 13:38:36 -0400 Subject: [PATCH 3/7] add autofill tests --- .../src/engine/text_editing/text_editing.dart | 16 +-- lib/web_ui/test/engine/text_editing_test.dart | 114 +++++++++++++++++- 2 files changed, 115 insertions(+), 15 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index d46d085aa440e..48f563b392177 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -183,13 +183,13 @@ void _insertEditingElementInView(DomElement element, int viewId) { /// static method. class EngineAutofillForm { EngineAutofillForm({ - required int viewId, + required this.viewId, required this.formElement, this.elements, this.items, this.formIdentifier = '', this.insertionReferenceNode, - }) : _viewId = viewId; + }); final DomHTMLFormElement formElement; @@ -207,15 +207,8 @@ class EngineAutofillForm { /// See [formsOnTheDom]. final String formIdentifier; - int _viewId; - int get viewId => _viewId; - set viewId(int value) { - if (_viewId == value) { - return; - } - _viewId = value; - _ensureEditingElementInView(formElement, _viewId); - } + /// 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. @@ -1342,7 +1335,6 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements final AutofillInfo? autofill = config.autofill; if (autofill != null) { autofill.applyToDomElement(activeDomElement, focusedElement: true); - config.autofillGroup!.viewId = config.viewId; } else { activeDomElement.setAttribute('autocomplete', 'off'); // When the new input configuration contains a different view ID, we need diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index cfd6b8cc7995b..110a0909da63b 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -2486,13 +2486,14 @@ Future testMain() async { host.remove(); }); - test('move element to correct view', () { + test('moves element to correct view', () { final DomElement host1 = createDomElement('div'); - final DomElement host2 = createDomElement('div'); domDocument.body!.append(host1); - domDocument.body!.append(host2); 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); @@ -2524,6 +2525,113 @@ Future testMain() async { 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 flutterMultiAutofillElementConfig = + createFlutterConfig( + 'text', + viewId: view.viewId, + autofillHint: 'username', + autofillHintsForFields: ['username', 'email', 'name'], + ); + final MethodCall setClient = MethodCall( + 'TextInput.setClient', + [123, flutterMultiAutofillElementConfig], + ); + 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 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 autofillConfig1 = createFlutterConfig( + 'text', + viewId: view1.viewId, + autofillHint: 'username', + autofillHintsForFields: ['username', 'email', 'name'], + ); + final MethodCall setClient = MethodCall( + 'TextInput.setClient', + [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 autofillConfig2 = createFlutterConfig( + 'text', + viewId: view2.viewId, + autofillHint: 'username', + autofillHintsForFields: ['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(); }); From e508ea7ada574cf258b3474ec51a6816e76c7e90 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Wed, 13 Mar 2024 14:14:41 -0400 Subject: [PATCH 4/7] view root selector --- lib/web_ui/lib/src/engine/util.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index 6edc1b269eb19..c3e9e751b8a74 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -651,12 +651,12 @@ int? tryViewId(Object? arguments) { return null; } +const String _viewRootSelector = + '${DomManager.flutterViewTagName}[${GlobalHtmlAttributes.flutterViewIdAttributeName}]'; + int? findParentViewId(DomElement element) { - DomElement? current = element; - while (current != null && element.tagName.toLowerCase() != DomManager.flutterViewTagName) { - current = element.parent; - } - final String? viewIdAttribute = current?.getAttribute(GlobalHtmlAttributes.flutterViewIdAttributeName); + final DomElement? viewRoot = element.closest(_viewRootSelector); + final String? viewIdAttribute = viewRoot?.getAttribute(GlobalHtmlAttributes.flutterViewIdAttributeName); return viewIdAttribute == null ? null : int.parse(viewIdAttribute); } From 7b151168edae0d6cb3dd246e297f0426c53a49a8 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Wed, 13 Mar 2024 14:41:10 -0400 Subject: [PATCH 5/7] actually we don't need this util --- lib/web_ui/lib/src/engine/util.dart | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index 488623c5d79fc..1889e41274763 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -15,8 +15,6 @@ import 'dom.dart'; import 'safe_browser_api.dart'; import 'services.dart'; import 'vector_math.dart'; -import 'view_embedder/dom_manager.dart'; -import 'view_embedder/global_html_attributes.dart'; /// Generic callback signature, used by [_futurize]. typedef Callback = void Function(T result); @@ -664,15 +662,6 @@ int? tryViewId(Object? arguments) { return null; } -const String _viewRootSelector = - '${DomManager.flutterViewTagName}[${GlobalHtmlAttributes.flutterViewIdAttributeName}]'; - -int? findParentViewId(DomElement element) { - final DomElement? viewRoot = element.closest(_viewRootSelector); - final String? viewIdAttribute = viewRoot?.getAttribute(GlobalHtmlAttributes.flutterViewIdAttributeName); - return viewIdAttribute == null ? null : int.parse(viewIdAttribute); -} - /// Prints a list of bytes in hex format. /// /// Bytes are separated by one space and are padded on the left to always show From 1941ece863ee58f6cb9ba82ff2622d392761f357 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Thu, 14 Mar 2024 10:31:21 -0400 Subject: [PATCH 6/7] use _insertEditingElementInView more --- lib/web_ui/lib/src/engine/text_editing/text_editing.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 48f563b392177..e8ee5755a2eda 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -1279,8 +1279,6 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements DomHTMLFormElement? get focusedFormElement => inputConfiguration.autofillGroup?.formElement; - FlutterViewManager get viewManager => EnginePlatformDispatcher.instance.viewManager; - @override void initializeTextEditing( InputConfiguration inputConfig, { @@ -1300,8 +1298,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. - final DomElement textEditingHost = viewManager[inputConfig.viewId]!.dom.textEditingHost; - textEditingHost.append(activeDomElement); + _insertEditingElementInView(activeDomElement, inputConfig.viewId); _appendedToForm = false; } @@ -1795,8 +1792,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy { if (hasAutofillGroup) { placeForm(); } else { - final DomElement textEditingHost = viewManager[inputConfig.viewId]!.dom.textEditingHost; - textEditingHost.append(activeDomElement); + _insertEditingElementInView(activeDomElement, inputConfig.viewId); } inputConfig.textCapitalization.setAutocapitalizeAttribute( activeDomElement); From bffe3080b185585132fd2c3f2f5cc13a9495661f Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Fri, 29 Mar 2024 11:57:09 -0400 Subject: [PATCH 7/7] address comments --- .../src/engine/text_editing/text_editing.dart | 12 ++++----- .../view_embedder/flutter_view_manager.dart | 25 ++++++------------- lib/web_ui/test/engine/text_editing_test.dart | 15 ++++++++++- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 7562a1d80034e..ac3c7b187fedf 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -167,13 +167,11 @@ void _ensureEditingElementInView(DomElement element, int viewId) { void _insertEditingElementInView(DomElement element, int viewId) { final FlutterViewManager viewManager = EnginePlatformDispatcher.instance.viewManager; final EngineFlutterView? view = viewManager[viewId]; - if (view == null) { - // `viewId` points to a non-existent view, is this even possible? Should we - // throw? - return; - } - - view.dom.textEditingHost.append(element); + 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. diff --git a/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart b/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart index 7553841bc0331..2d2bc639e361a 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart @@ -17,10 +17,6 @@ class FlutterViewManager { final Map _jsViewOptions = {}; - // A map of root elements to their corresponding EngineFlutterView. - final Map _elementToView = - {}; - // The controller of the [onViewCreated] stream. final StreamController _onViewCreatedController = StreamController.broadcast(sync: true); @@ -67,7 +63,6 @@ class FlutterViewManager { // Store the view, and the jsViewOptions, if any... _viewData[viewId] = view; - _elementToView[view.dom.rootElement] = view; if (jsViewOptions != null) { _jsViewOptions[viewId] = jsViewOptions; } @@ -90,10 +85,7 @@ class FlutterViewManager { /// /// Returns its [JsFlutterViewOptions] (if any). JsFlutterViewOptions? unregisterView(int viewId) { - final EngineFlutterView? unregisteredView = _viewData.remove(viewId); - if (unregisteredView != null) { - _elementToView.remove(unregisteredView.dom.rootElement); - } + _viewData.remove(viewId); final JsFlutterViewOptions? jsViewOptions = _jsViewOptions.remove(viewId); _onViewDisposedController.add(viewId); return jsViewOptions; @@ -108,15 +100,12 @@ class FlutterViewManager { } EngineFlutterView? findViewForElement(DomElement? element) { - DomElement? current = element; - while (current != null) { - final EngineFlutterView? view = _elementToView[current]; - if (view != null) { - return view; - } - current = current.parent; - } - return null; + 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() { diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index 9ed16a2800aac..1d9b026f2414f 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -2523,7 +2523,7 @@ Future testMain() async { expect(event.defaultPrevented, isFalse); }); - test('inserts element in the correct view', () { + test('inserts element in the correct view', () async { final DomElement host = createDomElement('div'); domDocument.body!.append(host); final EngineFlutterView view = EngineFlutterView(dispatcher, host); @@ -2531,9 +2531,17 @@ Future testMain() async { 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 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); @@ -2607,6 +2615,11 @@ Future testMain() async { 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 transform = Matrix4.identity().storage.toList(); + final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(10, 10, transform); + sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform)); await waitForDesktopSafariFocus();