[web] New HtmlElementView.fromTagName constructor (#130513)
This wraps up the platform view improvements discussed in https://github.com/flutter/flutter/issues/127030. - Splits `HtmlElementView` into 2 files that are conditionally imported. - The non-web version can be instantiated but it throws if it ends up being built in a widget tree. - Out-of-the-box view factories that create visible & invisible DOM elements given a `tagName` parameter. - New `HtmlElementView.fromTagName()` constructor that uses the default factories to create DOM elements. - Tests covering the new API. Depends on https://github.com/flutter/engine/pull/43828 Fixes https://github.com/flutter/flutter/issues/127030
This commit is contained in:
parent
f054f5aa09
commit
55fe41be59
34
packages/flutter/lib/src/widgets/_html_element_view_io.dart
Normal file
34
packages/flutter/lib/src/widgets/_html_element_view_io.dart
Normal file
@ -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');
|
||||
}
|
||||
}
|
136
packages/flutter/lib/src/widgets/_html_element_view_web.dart
Normal file
136
packages/flutter/lib/src/widgets/_html_element_view_web.dart
Normal file
@ -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: <dynamic, dynamic>{'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 <Factory<OneSequenceGestureRecognizer>>{},
|
||||
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<void> _initialize() async {
|
||||
final Map<String, dynamic> args = <String, dynamic>{
|
||||
'id': viewId,
|
||||
'viewType': viewType,
|
||||
'params': creationParams,
|
||||
};
|
||||
await SystemChannels.platform_views.invokeMethod<void>('create', args);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearFocus() async {
|
||||
// Currently this does nothing on Flutter Web.
|
||||
// TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<void> dispose() async {
|
||||
if (_initialized) {
|
||||
await SystemChannels.platform_views.invokeMethod<void>('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;
|
@ -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<UiKitView> 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 <Factory<OneSequenceGestureRecognizer>>{},
|
||||
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<void> _initialize() async {
|
||||
final Map<String, dynamic> args = <String, dynamic>{
|
||||
'id': viewId,
|
||||
'viewType': viewType,
|
||||
'params': creationParams,
|
||||
};
|
||||
await SystemChannels.platform_views.invokeMethod<void>('create', args);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearFocus() async {
|
||||
// Currently this does nothing on Flutter Web.
|
||||
// TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<void> dispose() async {
|
||||
if (_initialized) {
|
||||
await SystemChannels.platform_views.invokeMethod<void>('dispose', viewId);
|
||||
}
|
||||
}
|
||||
Widget build(BuildContext context) => buildImpl(context);
|
||||
}
|
||||
|
||||
class _AndroidViewState extends State<AndroidView> {
|
||||
|
@ -471,77 +471,6 @@ class FakeIosPlatformViewsController {
|
||||
}
|
||||
}
|
||||
|
||||
class FakeHtmlPlatformViewsController {
|
||||
FakeHtmlPlatformViewsController() {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform_views, _onMethodCall);
|
||||
}
|
||||
|
||||
Iterable<FakeHtmlPlatformView> get views => _views.values;
|
||||
final Map<int, FakeHtmlPlatformView> _views = <int, FakeHtmlPlatformView>{};
|
||||
|
||||
final Set<String> _registeredViewTypes = <String>{};
|
||||
|
||||
late Completer<void> resizeCompleter;
|
||||
|
||||
Completer<void>? createCompleter;
|
||||
|
||||
void registerViewType(String viewType) {
|
||||
_registeredViewTypes.add(viewType);
|
||||
}
|
||||
|
||||
Future<dynamic> _onMethodCall(MethodCall call) {
|
||||
switch (call.method) {
|
||||
case 'create':
|
||||
return _create(call);
|
||||
case 'dispose':
|
||||
return _dispose(call);
|
||||
}
|
||||
return Future<dynamic>.sync(() => null);
|
||||
}
|
||||
|
||||
Future<dynamic> _create(MethodCall call) async {
|
||||
final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>;
|
||||
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<int?>.sync(() => null);
|
||||
}
|
||||
|
||||
Future<dynamic> _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<dynamic>.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)';
|
||||
}
|
||||
}
|
||||
|
@ -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<Object?, Object?>;
|
||||
return web.document.createElement(params['tagName']! as String);
|
||||
});
|
||||
fakePlatformViewRegistry.registerViewFactory(ui_web.PlatformViewRegistry.defaultInvisibleViewType, (int viewId, {Object? params}) {
|
||||
params!;
|
||||
params as Map<Object?, Object?>;
|
||||
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>[
|
||||
FakeHtmlPlatformView(currentViewId + 1, 'webview'),
|
||||
fakePlatformViewRegistry.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
(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>[
|
||||
FakeHtmlPlatformView(currentViewId + 1, 'webview'),
|
||||
fakePlatformViewRegistry.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
(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: <Widget>[
|
||||
@ -101,18 +124,17 @@ void main() {
|
||||
);
|
||||
|
||||
expect(
|
||||
viewsController.views,
|
||||
unorderedEquals(<FakeHtmlPlatformView>[
|
||||
FakeHtmlPlatformView(currentViewId + 1, 'webview', 'foobar'),
|
||||
FakeHtmlPlatformView(currentViewId + 2, 'webview', 123),
|
||||
fakePlatformViewRegistry.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
(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<void>();
|
||||
final Completer<void> resizeCompleter = Completer<void>();
|
||||
|
||||
await tester.pumpWidget(
|
||||
const Center(
|
||||
@ -135,22 +157,21 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
viewsController.resizeCompleter.complete();
|
||||
resizeCompleter.complete();
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
viewsController.views,
|
||||
unorderedEquals(<FakeHtmlPlatformView>[
|
||||
FakeHtmlPlatformView(currentViewId + 1, 'webview'),
|
||||
fakePlatformViewRegistry.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
(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>[
|
||||
FakeHtmlPlatformView(currentViewId + 2, 'maps'),
|
||||
fakePlatformViewRegistry.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
(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>[
|
||||
FakeHtmlPlatformView(currentViewId + 1, 'webview'),
|
||||
fakePlatformViewRegistry.views,
|
||||
unorderedEquals(<FakePlatformView>[
|
||||
(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, <dynamic, dynamic>{'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, <dynamic, dynamic>{'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<Object> createdElements = <Object>[];
|
||||
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<FakePlatformView> get views => Set<FakePlatformView>.unmodifiable(_views);
|
||||
final Set<FakePlatformView> _views = <FakePlatformView>{};
|
||||
|
||||
final Set<FakeViewFactory> _registeredViewTypes = <FakeViewFactory>{};
|
||||
|
||||
@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<dynamic> _onMethodCall(MethodCall call) {
|
||||
switch (call.method) {
|
||||
case 'create':
|
||||
return _create(call);
|
||||
case 'dispose':
|
||||
return _dispose(call);
|
||||
}
|
||||
return Future<dynamic>.sync(() => null);
|
||||
}
|
||||
|
||||
Future<dynamic> _create(MethodCall call) async {
|
||||
final Map<dynamic, dynamic> args = call.arguments as Map<dynamic, dynamic>;
|
||||
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<dynamic> _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;
|
||||
}
|
||||
}
|
||||
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user