blob: d2778546cd57b715f714a2f793ecabd05a9a46d7 [file] [log] [blame]
// 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.
@TestOn('chrome')
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;
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 factories.
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();
fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview'),
),
),
);
expect(
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();
fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory);
bool hasPlatformViewCreated = false;
void onPlatformViewCreatedCallBack(int id) {
hasPlatformViewCreated = true;
}
await tester.pumpWidget(
Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(
viewType: 'webview',
onPlatformViewCreated: onPlatformViewCreatedCallBack,
),
),
),
);
// Check the onPlatformViewCreatedCallBack has been called.
expect(hasPlatformViewCreated, true);
expect(
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();
fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory);
await tester.pumpWidget(
const Column(
children: <Widget>[
SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(
viewType: 'webview',
creationParams: 'foobar',
),
),
SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(
viewType: 'webview',
creationParams: 123,
),
),
],
),
);
expect(
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();
fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview'),
),
),
);
final Completer<void> resizeCompleter = Completer<void>();
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 100.0,
height: 50.0,
child: HtmlElementView(viewType: 'webview'),
),
),
);
resizeCompleter.complete();
await tester.pump();
expect(
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();
fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory);
fakePlatformViewRegistry.registerViewFactory('maps', _mockViewFactory);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview'),
),
),
);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'maps'),
),
),
);
expect(
fakePlatformViewRegistry.views,
unorderedEquals(<FakePlatformView>[
(id: currentViewId + 2, viewType: 'maps', params: null, htmlElement: _mockHtmlElement),
]),
);
});
testWidgets('Dispose HTML view', (WidgetTester tester) async {
fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview'),
),
),
);
await tester.pumpWidget(
const Center(
child: SizedBox(
width: 200.0,
height: 100.0,
),
),
);
expect(
fakePlatformViewRegistry.views,
isEmpty,
);
});
testWidgets('HTML view survives widget tree change', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory);
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview', key: key),
),
),
);
await tester.pumpWidget(
Center(
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(viewType: 'webview', key: key),
),
),
);
expect(
fakePlatformViewRegistry.views,
unorderedEquals(<FakePlatformView>[
(id: currentViewId + 1, viewType: 'webview', params: null, htmlElement: _mockHtmlElement),
]),
);
});
testWidgets('HtmlElementView has correct semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
expect(currentViewId, greaterThanOrEqualTo(0));
fakePlatformViewRegistry.registerViewFactory('webview', _mockViewFactory);
await tester.pumpWidget(
Semantics(
container: true,
child: const Align(
alignment: Alignment.bottomRight,
child: SizedBox(
width: 200.0,
height: 100.0,
child: HtmlElementView(
viewType: 'webview',
),
),
),
),
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
// The platform view ID is set on the child of the HtmlElementView render object.
final SemanticsNode semantics = tester.getSemantics(find.byType(PlatformViewSurface));
expect(semantics.platformViewId, currentViewId + 1);
expect(semantics.rect, const Rect.fromLTWH(0, 0, 200, 100));
// A 200x100 rect positioned at bottom right of a 800x600 box.
expect(semantics.transform, Matrix4.translationValues(600, 500, 0));
expect(semantics.childrenCount, 0);
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) {
return switch (call.method) {
'create' => _create(call),
'dispose' => _dispose(call),
_ => 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;
}
}