| // 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:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| 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; |
| } |
| |
| 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(() { |
| ui_web.debugOverridePlatformViewRegistry(fakePlatformViewRegistry); |
| }); |
| |
| tearDown(() { |
| ui_web.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); |
| }); |
| |
| group('hitTestBehavior', () { |
| testWidgets('opaque by default', (WidgetTester tester) async { |
| final Key containerKey = UniqueKey(); |
| int taps = 0; |
| |
| await tester.pumpWidget( |
| GestureDetector( |
| onTap: () => taps++, |
| child: Container( |
| key: containerKey, |
| width: 200, |
| height: 200, |
| // Add a color to make it a visible container. This ensures that |
| // GestureDetector's default hit test behavior works. |
| color: const Color(0xFF00FF00), |
| child: HtmlElementView.fromTagName(tagName: 'div'), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(taps, isZero); |
| |
| await tester.tap(find.byKey(containerKey), warnIfMissed: false); |
| |
| // Taps are still zero on the container because the HtmlElementView is |
| // opaque and prevents widgets behind it from receiving pointer events. |
| expect(taps, isZero); |
| }); |
| |
| testWidgets('can be set to transparent', (WidgetTester tester) async { |
| final Key containerKey = UniqueKey(); |
| int taps = 0; |
| |
| await tester.pumpWidget( |
| GestureDetector( |
| onTap: () => taps++, |
| child: Container( |
| key: containerKey, |
| width: 200, |
| height: 200, |
| // Add a color to make it a visible container. This ensures that |
| // GestureDetector's default hit test behavior works. |
| color: const Color(0xFF00FF00), |
| child: HtmlElementView.fromTagName( |
| tagName: 'div', |
| hitTestBehavior: PlatformViewHitTestBehavior.transparent, |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(taps, isZero); |
| |
| await tester.tap(find.byKey(containerKey), warnIfMissed: false); |
| |
| // The container can receive taps because the HtmlElementView is |
| // transparent from a hit testing perspective. |
| expect(taps, 1); |
| }); |
| }); |
| }); |
| } |