diff --git a/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_io.dart b/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_io.dart index e1efce68a2..b1035f3460 100644 --- a/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_io.dart +++ b/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_io.dart @@ -46,6 +46,12 @@ class PlatformSelectableRegionContextMenu extends StatelessWidget { @visibleForTesting static RegisterViewFactory? debugOverrideRegisterViewFactory; + /// Resets the view factory registration to its initial state. + @visibleForTesting + static void debugResetRegistry() { + throw UnimplementedError(); + } + @override Widget build(BuildContext context) => throw UnimplementedError(); } diff --git a/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart b/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart index 56b136190a..33b7e264a7 100644 --- a/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart +++ b/packages/flutter/lib/src/widgets/_platform_selectable_region_context_menu_web.dart @@ -75,6 +75,12 @@ class PlatformSelectableRegionContextMenu extends StatelessWidget { @visibleForTesting static RegisterViewFactory? debugOverrideRegisterViewFactory; + /// Resets the view factory registration to its initial state. + @visibleForTesting + static void debugResetRegistry() { + _registeredViewType = null; + } + // Registers the view factories for the interceptor widgets. static void _register() { assert(_registeredViewType == null); @@ -104,21 +110,21 @@ class PlatformSelectableRegionContextMenu extends StatelessWidget { } static String _registerWebSelectionCallback(_WebSelectionCallBack callback) { - _registerViewFactory(_viewType, (int viewId) { + // Create css style for _kClassName. + final web.HTMLStyleElement styleElement = + web.document.createElement('style') as web.HTMLStyleElement; + web.document.head!.append(styleElement as JSAny); + final web.CSSStyleSheet sheet = styleElement.sheet!; + sheet.insertRule(_kClassRule, 0); + sheet.insertRule(_kClassSelectionRule, 1); + + _registerViewFactory(_viewType, (int viewId, {Object? params}) { final web.HTMLElement htmlElement = web.document.createElement('div') as web.HTMLElement; htmlElement ..style.width = '100%' ..style.height = '100%' ..classList.add(_kClassName); - // Create css style for _kClassName. - final web.HTMLStyleElement styleElement = - web.document.createElement('style') as web.HTMLStyleElement; - web.document.head!.append(styleElement as JSAny); - final web.CSSStyleSheet sheet = styleElement.sheet!; - sheet.insertRule(_kClassRule, 0); - sheet.insertRule(_kClassSelectionRule, 1); - htmlElement.addEventListener( 'mousedown', (web.Event event) { diff --git a/packages/flutter/test/widgets/html_element_view_test.dart b/packages/flutter/test/widgets/html_element_view_test.dart index 78aaf45b9d..08421b89d3 100644 --- a/packages/flutter/test/widgets/html_element_view_test.dart +++ b/packages/flutter/test/widgets/html_element_view_test.dart @@ -8,7 +8,6 @@ library; import 'dart:async'; import 'dart:ui_web' as ui_web; -import 'package:collection/collection.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/src/widgets/_html_element_view_web.dart' @@ -17,6 +16,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:web/web.dart' as web; +import 'web_platform_view_registry_utils.dart'; + final Object _mockHtmlElement = Object(); Object _mockViewFactory(int id, {Object? params}) { return _mockHtmlElement; @@ -419,101 +420,3 @@ void main() { }); }); } - -typedef FakeViewFactory = ({String viewType, bool isVisible, Function viewFactory}); - -typedef FakePlatformView = ({int id, String viewType, Object? params, Object htmlElement}); - -class FakePlatformViewRegistry implements ui_web.PlatformViewRegistry { - FakePlatformViewRegistry() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - SystemChannels.platform_views, - _onMethodCall, - ); - } - - Set get views => Set.unmodifiable(_views); - final Set _views = {}; - - final Set _registeredViewTypes = {}; - - @override - bool registerViewFactory(String viewType, Function viewFactory, {bool isVisible = true}) { - if (_findRegisteredViewFactory(viewType) != null) { - return false; - } - _registeredViewTypes.add((viewType: viewType, isVisible: isVisible, viewFactory: viewFactory)); - return true; - } - - @override - Object getViewById(int viewId) { - return _findViewById(viewId)!.htmlElement; - } - - FakeViewFactory? _findRegisteredViewFactory(String viewType) { - return _registeredViewTypes.singleWhereOrNull( - (FakeViewFactory registered) => registered.viewType == viewType, - ); - } - - FakePlatformView? _findViewById(int viewId) { - return _views.singleWhereOrNull((FakePlatformView view) => view.id == viewId); - } - - Future _onMethodCall(MethodCall call) { - return switch (call.method) { - 'create' => _create(call), - 'dispose' => _dispose(call), - _ => Future.sync(() => null), - }; - } - - Future _create(MethodCall call) async { - final Map args = call.arguments as Map; - final int id = args['id'] as int; - final String viewType = args['viewType'] as String; - final Object? params = args['params']; - - if (_findViewById(id) != null) { - throw PlatformException( - code: 'error', - message: 'Trying to create an already created platform view, view id: $id', - ); - } - - final FakeViewFactory? registered = _findRegisteredViewFactory(viewType); - if (registered == null) { - throw PlatformException( - code: 'error', - message: 'Trying to create a platform view of unregistered type: $viewType', - ); - } - - final ui_web.ParameterizedPlatformViewFactory viewFactory = - registered.viewFactory as ui_web.ParameterizedPlatformViewFactory; - - _views.add(( - id: id, - viewType: viewType, - params: params, - htmlElement: viewFactory(id, params: params), - )); - return null; - } - - Future _dispose(MethodCall call) async { - final int id = call.arguments as int; - - final FakePlatformView? view = _findViewById(id); - if (view == null) { - throw PlatformException( - code: 'error', - message: 'Trying to dispose a platform view with unknown id: $id', - ); - } - - _views.remove(view); - return null; - } -} diff --git a/packages/flutter/test/widgets/selectable_region_context_menu_test.dart b/packages/flutter/test/widgets/selectable_region_context_menu_test.dart index 050a8083ac..c9be2665d9 100644 --- a/packages/flutter/test/widgets/selectable_region_context_menu_test.dart +++ b/packages/flutter/test/widgets/selectable_region_context_menu_test.dart @@ -5,14 +5,15 @@ @TestOn('browser') // This file contains web-only library. library; -import 'dart:js_interop'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:web/web.dart' as web; +import 'web_platform_view_registry_utils.dart'; + extension on web.HTMLCollection { Iterable get iterable => Iterable.generate(length, (int index) => item(index)); @@ -24,47 +25,75 @@ extension on web.CSSRuleList { } void main() { - web.HTMLElement? element; - PlatformSelectableRegionContextMenu.debugOverrideRegisterViewFactory = ( - String viewType, - Object Function(int viewId) fn, { - bool isVisible = true, - }) { - element = fn(0) as web.HTMLElement; - // The element needs to be attached to the document body to receive mouse - // events. - web.document.body!.append(element! as JSAny); - }; - // This force register the dom element. - PlatformSelectableRegionContextMenu(child: const Placeholder()); - PlatformSelectableRegionContextMenu.debugOverrideRegisterViewFactory = null; + late FakePlatformViewRegistry fakePlatformViewRegistry; + + setUp(() { + removeAllStyleElements(); + fakePlatformViewRegistry = FakePlatformViewRegistry(); + PlatformSelectableRegionContextMenu.debugOverrideRegisterViewFactory = + fakePlatformViewRegistry.registerViewFactory; + }); + + tearDown(() { + PlatformSelectableRegionContextMenu.debugOverrideRegisterViewFactory = null; + PlatformSelectableRegionContextMenu.debugResetRegistry(); + }); + + testWidgets('DOM element is set up correctly', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + + await tester.pumpWidget( + MaterialApp( + home: SelectableRegion( + selectionControls: EmptyTextSelectionControls(), + child: const Placeholder(), + ), + ), + ); + + final web.HTMLElement element = + fakePlatformViewRegistry.getViewById(currentViewId + 1) as web.HTMLElement; - test('DOM element is set up correctly', () async { expect(element, isNotNull); - expect(element!.style.width, '100%'); - expect(element!.style.height, '100%'); - expect(element!.classList.length, 1); - final String className = element!.className; + expect(element.style.width, '100%'); + expect(element.style.height, '100%'); + expect(element.classList.length, 1); - expect(web.document.head!.children.iterable, isNotEmpty); - bool foundStyle = false; - for (final web.Element? element in web.document.head!.children.iterable) { - expect(element, isNotNull); - if (element!.tagName != 'STYLE') { - continue; - } - final web.CSSRuleList? rules = (element as web.HTMLStyleElement).sheet?.rules; - if (rules != null) { - foundStyle = rules.iterable.any((web.CSSRule? rule) => rule!.cssText.contains(className)); - } - if (foundStyle) { - break; - } - } - expect(foundStyle, isTrue); + final int numberOfStyleElements = getNumberOfStyleElements(); + expect(numberOfStyleElements, 1); + }); + + testWidgets('only one