// 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:leak_tracker_flutter_testing/leak_tracker_flutter_testing.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 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', () { testWidgetsWithLeakTracking('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), ]), ); }); testWidgetsWithLeakTracking('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), ]), ); }); testWidgetsWithLeakTracking('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), ]), ); }); testWidgetsWithLeakTracking('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), ]), ); }); testWidgetsWithLeakTracking('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), ]), ); }); testWidgetsWithLeakTracking('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, ); }); testWidgetsWithLeakTracking('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), ]), ); }); testWidgetsWithLeakTracking('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; }); testWidgetsWithLeakTracking('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')); }); testWidgetsWithLeakTracking('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')); }); testWidgetsWithLeakTracking('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; } }