// 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' show PointerDeviceKind; import 'dart:ui_web' as ui_web; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.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; 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); }, ); fakePlatformViewRegistry.registerViewFactory('Browser__WebContextMenuViewType__', ( int viewId, { Object? params, }) { final htmlElement = web.document.createElement('div') as web.HTMLElement; htmlElement ..style.width = '100%' ..style.height = '100%' ..classList.add('web-selectable-region-context-menu'); return htmlElement; }); }); 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([ (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); var 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([ (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: [ 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([ ( 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 resizeCompleter = Completer(); 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([ (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([ (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([ (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, {'tagName': 'div'}); // The HTML element should be a div. final 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 htmlElement = fakePlatformView.htmlElement as web.HTMLElement; expect(htmlElement.tagName, equalsIgnoringCase('script')); }); testWidgets('onElementCreated', (WidgetTester tester) async { final 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); }); group('hitTestBehavior', () { testWidgets('opaque by default', (WidgetTester tester) async { final Key containerKey = UniqueKey(); var 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(); var 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); }); }); }); // Regression test for https://github.com/flutter/flutter/issues/174246 // There is a control case for non-Web in selection_area_test.dart. testWidgets('SelectionArea applies correct mouse cursors in its empty region on Web', ( WidgetTester tester, ) async { final GlobalKey innerRegion = GlobalKey(); await tester.pumpWidget( MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( // Region 1 (fullscreen) body: MouseRegion( cursor: SystemMouseCursors.grab, child: Center( child: Container( decoration: BoxDecoration(border: Border.all()), // Region 2 (SelectionArea) child: SelectionArea( child: Padding( padding: const EdgeInsetsGeometry.all(40), // Region 3 (inner MouseRegion) child: MouseRegion( key: innerRegion, cursor: SystemMouseCursors.forbidden, onHover: (_) {}, child: Container(color: const Color(0xFFAA9933), width: 200, height: 50), ), ), ), ), ), ), ), ), ); // Initialize the HtmlElementView inside SelectionArea. await tester.pump(); // Ensure that the HtmlElementView is initialized. expect( find.byWidgetPredicate( (Widget widget) => widget.toString().contains('_PlatformViewPlaceHolder'), ), findsNothing, ); const region1 = Offset(10, 10); final Offset region2 = tester.getTopLeft(find.byKey(innerRegion)) - const Offset(3, 3); final Offset region3 = tester.getCenter(find.byKey(innerRegion)); final TestGesture gesture = await tester.startGesture(region1, kind: PointerDeviceKind.mouse); addTearDown(gesture.removePointer); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab, ); await gesture.moveTo(region2); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab, ); await gesture.moveTo(region3); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden, ); await gesture.moveTo(region2); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab, ); }); }