diff --git a/packages/flutter/lib/src/widgets/_html_element_view_io.dart b/packages/flutter/lib/src/widgets/_html_element_view_io.dart new file mode 100644 index 0000000000..e1c23e2d9e --- /dev/null +++ b/packages/flutter/lib/src/widgets/_html_element_view_io.dart @@ -0,0 +1,34 @@ +// Copyright 2014 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. + +// ignore_for_file: prefer_const_constructors_in_immutables +// ignore_for_file: avoid_unused_constructor_parameters + +import 'framework.dart'; +import 'platform_view.dart'; + +/// The platform-specific implementation of [HtmlElementView]. +extension HtmlElementViewImpl on HtmlElementView { + /// Creates an [HtmlElementView] that renders a DOM element with the given + /// [tagName]. + static HtmlElementView createFromTagName({ + Key? key, + required String tagName, + bool isVisible = true, + ElementCreatedCallback? onElementCreated, + }) { + throw UnimplementedError('HtmlElementView is only available on Flutter Web'); + } + + /// Called from [HtmlElementView.build] to build the widget tree. + /// + /// This is not expected to be invoked in non-web environments. It throws if + /// that happens. + /// + /// The implementation on Flutter Web builds a platform view and handles its + /// lifecycle. + Widget buildImpl(BuildContext context) { + throw UnimplementedError('HtmlElementView is only available on Flutter Web'); + } +} diff --git a/packages/flutter/lib/src/widgets/_html_element_view_web.dart b/packages/flutter/lib/src/widgets/_html_element_view_web.dart new file mode 100644 index 0000000000..fa7060e6ad --- /dev/null +++ b/packages/flutter/lib/src/widgets/_html_element_view_web.dart @@ -0,0 +1,136 @@ +// Copyright 2014 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. + +import 'dart:ui_web' as ui_web; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +import 'framework.dart'; +import 'platform_view.dart'; + +/// The platform-specific implementation of [HtmlElementView]. +extension HtmlElementViewImpl on HtmlElementView { + /// Creates an [HtmlElementView] that renders a DOM element with the given + /// [tagName]. + static HtmlElementView createFromTagName({ + Key? key, + required String tagName, + bool isVisible = true, + ElementCreatedCallback? onElementCreated, + }) { + return HtmlElementView( + key: key, + viewType: isVisible ? ui_web.PlatformViewRegistry.defaultVisibleViewType : ui_web.PlatformViewRegistry.defaultInvisibleViewType, + onPlatformViewCreated: _createPlatformViewCallbackForElementCallback(onElementCreated), + creationParams: {'tagName': tagName}, + ); + } + + /// The implementation of [HtmlElementView.build]. + /// + /// This is not expected to be invoked in non-web environments. It throws if + /// that happens. + /// + /// The implementation on Flutter Web builds an HTML platform view and handles + /// its lifecycle. + Widget buildImpl(BuildContext context) { + return PlatformViewLink( + viewType: viewType, + onCreatePlatformView: _createController, + surfaceFactory: (BuildContext context, PlatformViewController controller) { + return PlatformViewSurface( + controller: controller, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + ); + } + + /// Creates the controller and kicks off its initialization. + _HtmlElementViewController _createController( + PlatformViewCreationParams params, + ) { + final _HtmlElementViewController controller = _HtmlElementViewController( + params.id, + viewType, + creationParams, + ); + controller._initialize().then((_) { + params.onPlatformViewCreated(params.id); + onPlatformViewCreated?.call(params.id); + }); + return controller; + } +} + +PlatformViewCreatedCallback? _createPlatformViewCallbackForElementCallback( + ElementCreatedCallback? onElementCreated, +) { + if (onElementCreated == null) { + return null; + } + return (int id) { + onElementCreated(_platformViewsRegistry.getViewById(id)); + }; +} + +class _HtmlElementViewController extends PlatformViewController { + _HtmlElementViewController( + this.viewId, + this.viewType, + this.creationParams, + ); + + @override + final int viewId; + + /// The unique identifier for the HTML view type to be embedded by this widget. + /// + /// A PlatformViewFactory for this type must have been registered. + final String viewType; + + final dynamic creationParams; + + bool _initialized = false; + + Future _initialize() async { + final Map args = { + 'id': viewId, + 'viewType': viewType, + 'params': creationParams, + }; + await SystemChannels.platform_views.invokeMethod('create', args); + _initialized = true; + } + + @override + Future clearFocus() async { + // Currently this does nothing on Flutter Web. + // TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496 + } + + @override + Future dispatchPointerEvent(PointerEvent event) async { + // We do not dispatch pointer events to HTML views because they may contain + // cross-origin iframes, which only accept user-generated events. + } + + @override + Future dispose() async { + if (_initialized) { + await SystemChannels.platform_views.invokeMethod('dispose', viewId); + } + } +} + +/// Overrides the [ui_web.PlatformViewRegistry] used by [HtmlElementView]. +/// +/// This is used for testing view factory registration. +@visibleForTesting +ui_web.PlatformViewRegistry? debugOverridePlatformViewRegistry; +ui_web.PlatformViewRegistry get _platformViewsRegistry => debugOverridePlatformViewRegistry ?? ui_web.platformViewRegistry; diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart index a780994ff6..96389c014b 100644 --- a/packages/flutter/lib/src/widgets/platform_view.dart +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -8,6 +8,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import '_html_element_view_io.dart' if (dart.library.js_util) '_html_element_view_web.dart'; import 'basic.dart'; import 'debug.dart'; import 'focus_manager.dart'; @@ -324,6 +325,14 @@ class UiKitView extends _DarwinView { State createState() => _UiKitViewState(); } +/// Callback signature for when the platform view's DOM element was created. +/// +/// [element] is the DOM element that was created. +/// +/// Also see [HtmlElementView.fromTagName] that uses this callback +/// signature. +typedef ElementCreatedCallback = void Function(Object element); + /// Embeds an HTML element in the Widget hierarchy in Flutter Web. /// /// *NOTE*: This only works in Flutter Web. To embed web content on other @@ -368,6 +377,52 @@ class HtmlElementView extends StatelessWidget { this.creationParams, }); + /// Creates a platform view that creates a DOM element specified by [tagName]. + /// + /// [isVisible] indicates whether the view is visible to the user or not. + /// Setting this to false allows the rendering pipeline to perform extra + /// optimizations knowing that the view will not result in any pixels painted + /// on the screen. + /// + /// [onElementCreated] is called when the DOM element is created. It can be + /// used by the app to customize the element by adding attributes and styles. + /// + /// ```dart + /// import 'package:flutter/widgets.dart'; + /// import 'package:web/web.dart' as web; + /// + /// // ... + /// + /// class MyWidget extends StatelessWidget { + /// const MyWidget({super.key}); + /// + /// @override + /// Widget build(BuildContext context) { + /// return HtmlElementView.fromTagName( + /// tagName: 'div', + /// onElementCreated: (Object element) { + /// element as web.HTMLElement; + /// element.style + /// ..backgroundColor = 'blue' + /// ..border = '1px solid red'; + /// }, + /// ); + /// } + /// } + /// ``` + factory HtmlElementView.fromTagName({ + Key? key, + required String tagName, + bool isVisible = true, + ElementCreatedCallback? onElementCreated, + }) => + HtmlElementViewImpl.createFromTagName( + key: key, + tagName: tagName, + isVisible: isVisible, + onElementCreated: onElementCreated, + ); + /// The unique identifier for the HTML view type to be embedded by this widget. /// /// A PlatformViewFactory for this type must have been registered. @@ -382,83 +437,7 @@ class HtmlElementView extends StatelessWidget { final Object? creationParams; @override - Widget build(BuildContext context) { - assert(kIsWeb, 'HtmlElementView is only available on Flutter Web.'); - return PlatformViewLink( - viewType: viewType, - onCreatePlatformView: _createHtmlElementView, - surfaceFactory: (BuildContext context, PlatformViewController controller) { - return PlatformViewSurface( - controller: controller, - gestureRecognizers: const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - ); - } - - /// Creates the controller and kicks off its initialization. - _HtmlElementViewController _createHtmlElementView(PlatformViewCreationParams params) { - final _HtmlElementViewController controller = _HtmlElementViewController( - params.id, - viewType, - creationParams, - ); - controller._initialize().then((_) { - params.onPlatformViewCreated(params.id); - onPlatformViewCreated?.call(params.id); - }); - return controller; - } -} - -class _HtmlElementViewController extends PlatformViewController { - _HtmlElementViewController( - this.viewId, - this.viewType, - this.creationParams, - ); - - @override - final int viewId; - - /// The unique identifier for the HTML view type to be embedded by this widget. - /// - /// A PlatformViewFactory for this type must have been registered. - final String viewType; - - final dynamic creationParams; - - bool _initialized = false; - - Future _initialize() async { - final Map args = { - 'id': viewId, - 'viewType': viewType, - 'params': creationParams, - }; - await SystemChannels.platform_views.invokeMethod('create', args); - _initialized = true; - } - - @override - Future clearFocus() async { - // Currently this does nothing on Flutter Web. - // TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496 - } - - @override - Future dispatchPointerEvent(PointerEvent event) async { - // We do not dispatch pointer events to HTML views because they may contain - // cross-origin iframes, which only accept user-generated events. - } - - @override - Future dispose() async { - if (_initialized) { - await SystemChannels.platform_views.invokeMethod('dispose', viewId); - } - } + Widget build(BuildContext context) => buildImpl(context); } class _AndroidViewState extends State { diff --git a/packages/flutter/test/services/fake_platform_views.dart b/packages/flutter/test/services/fake_platform_views.dart index 7d85e93733..e3dfdd5ada 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -471,77 +471,6 @@ class FakeIosPlatformViewsController { } } -class FakeHtmlPlatformViewsController { - FakeHtmlPlatformViewsController() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform_views, _onMethodCall); - } - - Iterable get views => _views.values; - final Map _views = {}; - - final Set _registeredViewTypes = {}; - - late Completer resizeCompleter; - - Completer? createCompleter; - - void registerViewType(String viewType) { - _registeredViewTypes.add(viewType); - } - - Future _onMethodCall(MethodCall call) { - switch (call.method) { - case 'create': - return _create(call); - case 'dispose': - return _dispose(call); - } - return 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 (_views.containsKey(id)) { - throw PlatformException( - code: 'error', - message: 'Trying to create an already created platform view, view id: $id', - ); - } - - if (!_registeredViewTypes.contains(viewType)) { - throw PlatformException( - code: 'error', - message: 'Trying to create a platform view of unregistered type: $viewType', - ); - } - - if (createCompleter != null) { - await createCompleter!.future; - } - - _views[id] = FakeHtmlPlatformView(id, viewType, params); - return Future.sync(() => null); - } - - Future _dispose(MethodCall call) { - final int id = call.arguments as int; - - if (!_views.containsKey(id)) { - throw PlatformException( - code: 'error', - message: 'Trying to dispose a platform view with unknown id: $id', - ); - } - - _views.remove(id); - return Future.sync(() => null); - } -} - @immutable class FakeAndroidPlatformView { const FakeAndroidPlatformView(this.id, this.type, this.size, this.layoutDirection, @@ -656,31 +585,3 @@ class FakeUiKitView { return 'FakeUiKitView(id: $id, type: $type, creationParams: $creationParams)'; } } - -@immutable -class FakeHtmlPlatformView { - const FakeHtmlPlatformView(this.id, this.type, [this.creationParams]); - - final int id; - final String type; - final Object? creationParams; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) { - return false; - } - return other is FakeHtmlPlatformView - && other.id == id - && other.type == type - && other.creationParams == creationParams; - } - - @override - int get hashCode => Object.hash(id, type, creationParams); - - @override - String toString() { - return 'FakeHtmlPlatformView(id: $id, type: $type, params: $creationParams)'; - } -} diff --git a/packages/flutter/test/widgets/html_element_view_test.dart b/packages/flutter/test/widgets/html_element_view_test.dart index b485aef6c9..dd8866e9fa 100644 --- a/packages/flutter/test/widgets/html_element_view_test.dart +++ b/packages/flutter/test/widgets/html_element_view_test.dart @@ -6,20 +6,45 @@ 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' + show debugOverridePlatformViewRegistry; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:web/web.dart' as web; -import '../services/fake_platform_views.dart'; +final Object _mockHtmlElement = Object(); +Object _mockViewFactory(int id, {Object? params}) { + return _mockHtmlElement; +} void main() { + late FakePlatformViewRegistry fakePlatformViewRegistry; + + setUp(() { + fakePlatformViewRegistry = FakePlatformViewRegistry(); + + // Simulate the engine registering default factores. + fakePlatformViewRegistry.registerViewFactory(ui_web.PlatformViewRegistry.defaultVisibleViewType, (int viewId, {Object? params}) { + params!; + params as Map; + return web.document.createElement(params['tagName']! as String); + }); + fakePlatformViewRegistry.registerViewFactory(ui_web.PlatformViewRegistry.defaultInvisibleViewType, (int viewId, {Object? params}) { + params!; + params as Map; + return web.document.createElement(params['tagName']! as String); + }); + }); + group('HtmlElementView', () { testWidgets('Create HTML view', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); await tester.pumpWidget( const Center( @@ -32,17 +57,16 @@ void main() { ); expect( - viewsController.views, - unorderedEquals([ - FakeHtmlPlatformView(currentViewId + 1, 'webview'), + fakePlatformViewRegistry.views, + unorderedEquals([ + (id: currentViewId + 1, viewType: 'webview', params: null, htmlElement: _mockHtmlElement), ]), ); }); testWidgets('Create HTML view with PlatformViewCreatedCallback', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); bool hasPlatformViewCreated = false; void onPlatformViewCreatedCallBack(int id) { @@ -66,17 +90,16 @@ void main() { expect(hasPlatformViewCreated, true); expect( - viewsController.views, - unorderedEquals([ - FakeHtmlPlatformView(currentViewId + 1, 'webview'), + fakePlatformViewRegistry.views, + unorderedEquals([ + (id: currentViewId + 1, viewType: 'webview', params: null, htmlElement: _mockHtmlElement), ]), ); }); testWidgets('Create HTML view with creation params', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); await tester.pumpWidget( const Column( children: [ @@ -101,18 +124,17 @@ void main() { ); expect( - viewsController.views, - unorderedEquals([ - FakeHtmlPlatformView(currentViewId + 1, 'webview', 'foobar'), - FakeHtmlPlatformView(currentViewId + 2, 'webview', 123), + fakePlatformViewRegistry.views, + unorderedEquals([ + (id: currentViewId + 1, viewType: 'webview', params: 'foobar', htmlElement: _mockHtmlElement), + (id: currentViewId + 2, viewType: 'webview', params: 123, htmlElement: _mockHtmlElement), ]), ); }); testWidgets('Resize HTML view', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); await tester.pumpWidget( const Center( child: SizedBox( @@ -123,7 +145,7 @@ void main() { ), ); - viewsController.resizeCompleter = Completer(); + final Completer resizeCompleter = Completer(); await tester.pumpWidget( const Center( @@ -135,22 +157,21 @@ void main() { ), ); - viewsController.resizeCompleter.complete(); + resizeCompleter.complete(); await tester.pump(); expect( - viewsController.views, - unorderedEquals([ - FakeHtmlPlatformView(currentViewId + 1, 'webview'), + fakePlatformViewRegistry.views, + unorderedEquals([ + (id: currentViewId + 1, viewType: 'webview', params: null, htmlElement: _mockHtmlElement), ]), ); }); testWidgets('Change HTML view type', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); - viewsController.registerViewType('maps'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); + fakePlatformViewRegistry.registerViewFactory('maps', _mockViewFactory); await tester.pumpWidget( const Center( child: SizedBox( @@ -172,16 +193,15 @@ void main() { ); expect( - viewsController.views, - unorderedEquals([ - FakeHtmlPlatformView(currentViewId + 2, 'maps'), + fakePlatformViewRegistry.views, + unorderedEquals([ + (id: currentViewId + 2, viewType: 'maps', params: null, htmlElement: _mockHtmlElement), ]), ); }); testWidgets('Dispose HTML view', (WidgetTester tester) async { - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); await tester.pumpWidget( const Center( child: SizedBox( @@ -202,15 +222,14 @@ void main() { ); expect( - viewsController.views, + fakePlatformViewRegistry.views, isEmpty, ); }); testWidgets('HTML view survives widget tree change', (WidgetTester tester) async { final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); final GlobalKey key = GlobalKey(); await tester.pumpWidget( Center( @@ -233,9 +252,9 @@ void main() { ); expect( - viewsController.views, - unorderedEquals([ - FakeHtmlPlatformView(currentViewId + 1, 'webview'), + fakePlatformViewRegistry.views, + unorderedEquals([ + (id: currentViewId + 1, viewType: 'webview', params: null, htmlElement: _mockHtmlElement), ]), ); }); @@ -244,8 +263,7 @@ void main() { final SemanticsHandle handle = tester.ensureSemantics(); final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); expect(currentViewId, greaterThanOrEqualTo(0)); - final FakeHtmlPlatformViewsController viewsController = FakeHtmlPlatformViewsController(); - viewsController.registerViewType('webview'); + fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory); await tester.pumpWidget( Semantics( @@ -278,4 +296,206 @@ void main() { handle.dispose(); }); }); + + group('HtmlElementView.fromTagName', () { + setUp(() { + debugOverridePlatformViewRegistry = fakePlatformViewRegistry; + }); + + tearDown(() { + debugOverridePlatformViewRegistry = null; + }); + + testWidgets('Create platform view from tagName', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: HtmlElementView.fromTagName(tagName: 'div'), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(fakePlatformViewRegistry.views, hasLength(1)); + final FakePlatformView fakePlatformView = fakePlatformViewRegistry.views.single; + expect(fakePlatformView.id, currentViewId + 1); + expect(fakePlatformView.viewType, ui_web.PlatformViewRegistry.defaultVisibleViewType); + expect(fakePlatformView.params, {'tagName': 'div'}); + + // The HTML element should be a div. + final web.HTMLElement htmlElement = fakePlatformView.htmlElement as web.HTMLElement; + expect(htmlElement.tagName, equalsIgnoringCase('div')); + }); + + testWidgets('Create invisible platform view', (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: HtmlElementView.fromTagName(tagName: 'script', isVisible: false), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(fakePlatformViewRegistry.views, hasLength(1)); + final FakePlatformView fakePlatformView = fakePlatformViewRegistry.views.single; + expect(fakePlatformView.id, currentViewId + 1); + // The view should be invisible. + expect(fakePlatformView.viewType, ui_web.PlatformViewRegistry.defaultInvisibleViewType); + expect(fakePlatformView.params, {'tagName': 'script'}); + + // The HTML element should be a script. + final web.HTMLElement htmlElement = fakePlatformView.htmlElement as web.HTMLElement; + expect(htmlElement.tagName, equalsIgnoringCase('script')); + }); + + testWidgets('onElementCreated', (WidgetTester tester) async { + final List createdElements = []; + void onElementCreated(Object element) { + createdElements.add(element); + } + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 200.0, + height: 100.0, + child: HtmlElementView.fromTagName( + tagName: 'table', + onElementCreated: onElementCreated, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(fakePlatformViewRegistry.views, hasLength(1)); + final FakePlatformView fakePlatformView = fakePlatformViewRegistry.views.single; + + expect(createdElements, hasLength(1)); + final Object createdElement = createdElements.single; + + expect(createdElement, fakePlatformView.htmlElement); + }); + }); +} + +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) { + switch (call.method) { + case 'create': + return _create(call); + case 'dispose': + return _dispose(call); + } + return 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/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart index 7b4bc72037..d9164b59c3 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -3189,7 +3189,7 @@ void main() { // This file runs on non-web platforms, so we expect `HtmlElementView` to // fail. final dynamic exception = tester.takeException(); - expect(exception, isAssertionError); + expect(exception, isUnimplementedError); expect(exception.toString(), contains('HtmlElementView is only available on Flutter Web')); }); }