// 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:async'; import 'dart:ui' as ui; import 'package:fake_async/fake_async.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../services/fake_platform_views.dart'; import 'rendering_tester.dart'; void main() { final TestRenderingFlutterBinding binding = TestRenderingFlutterBinding.ensureInitialized(); tearDown(() { // Lay out a dummy RenderBox to make sure that anything that was laid out // during the test gets detached. final RenderBox emptyRenderBox = RenderCustomPaint(painter: _EmptyPainter()); layout(emptyRenderBox); }); group('PlatformViewRenderBox', () { late FakePlatformViewController fakePlatformViewController; late PlatformViewRenderBox platformViewRenderBox; setUp(() { fakePlatformViewController = FakePlatformViewController(0); platformViewRenderBox = PlatformViewRenderBox( controller: fakePlatformViewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{ Factory(() { return VerticalDragGestureRecognizer(); }), }, ); }); test('layout should size to max constraint', () { layout(platformViewRenderBox); platformViewRenderBox.layout( const BoxConstraints(minWidth: 50, minHeight: 50, maxWidth: 100, maxHeight: 100), ); expect(platformViewRenderBox.size, const Size(100, 100)); }); test('send semantics update if id is changed', () { final tree = RenderConstrainedBox( additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0), child: platformViewRenderBox, ); var semanticsUpdateCount = 0; final SemanticsHandle semanticsHandle = TestRenderingFlutterBinding.instance .ensureSemantics(); TestRenderingFlutterBinding.instance.pipelineOwner.semanticsOwner!.addListener(() { ++semanticsUpdateCount; }); layout(tree, phase: EnginePhase.flushSemantics); // Initial semantics update expect(semanticsUpdateCount, 1); semanticsUpdateCount = 0; // Request semantics update even though nothing changed. platformViewRenderBox.markNeedsSemanticsUpdate(); pumpFrame(phase: EnginePhase.flushSemantics); expect(semanticsUpdateCount, 0); semanticsUpdateCount = 0; final updatedFakePlatformViewController = FakePlatformViewController(10); platformViewRenderBox.controller = updatedFakePlatformViewController; pumpFrame(phase: EnginePhase.flushSemantics); // Update id should update the semantics. expect(semanticsUpdateCount, 1); semanticsHandle.dispose(); }); test('mouse hover events are dispatched via PlatformViewController.dispatchPointerEvent', () { layout(platformViewRenderBox); pumpFrame(phase: EnginePhase.flushSemantics); RendererBinding.instance.platformDispatcher.onPointerDataPacket!( ui.PointerDataPacket( data: [ _pointerData(ui.PointerChange.add, Offset.zero), _pointerData(ui.PointerChange.hover, const Offset(10, 10)), _pointerData(ui.PointerChange.remove, const Offset(10, 10)), ], ), ); expect(fakePlatformViewController.dispatchedPointerEvents, isNotEmpty); }); test('touch hover events are dispatched via PlatformViewController.dispatchPointerEvent', () { layout(platformViewRenderBox); pumpFrame(phase: EnginePhase.flushSemantics); RendererBinding.instance.platformDispatcher.onPointerDataPacket!( ui.PointerDataPacket( data: [ _pointerData(ui.PointerChange.add, Offset.zero), _pointerData(ui.PointerChange.hover, const Offset(10, 10)), _pointerData(ui.PointerChange.remove, const Offset(10, 10)), ], ), ); expect(fakePlatformViewController.dispatchedPointerEvents, isNotEmpty); }); }); // Regression test for https://github.com/flutter/flutter/issues/69431 test('multi-finger touch test', () { final viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); final AndroidViewController viewController = PlatformViewsService.initAndroidView( id: 0, viewType: 'webview', layoutDirection: TextDirection.rtl, ); final platformViewRenderBox = PlatformViewRenderBox( controller: viewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{ Factory(() => VerticalDragGestureRecognizer()), }, ); layout(platformViewRenderBox); pumpFrame(phase: EnginePhase.flushSemantics); viewController.pointTransformer = (Offset offset) => platformViewRenderBox.globalToLocal(offset); FakeAsync().run((FakeAsync async) { // Put one pointer down. RendererBinding.instance.platformDispatcher.onPointerDataPacket!( ui.PointerDataPacket( data: [ _pointerData( ui.PointerChange.add, Offset.zero, pointer: 1, kind: PointerDeviceKind.touch, ), _pointerData( ui.PointerChange.down, const Offset(10, 10), pointer: 1, kind: PointerDeviceKind.touch, ), _pointerData( ui.PointerChange.remove, const Offset(10, 10), pointer: 1, kind: PointerDeviceKind.touch, ), ], ), ); async.flushMicrotasks(); // Put another pointer down and then cancel it. RendererBinding.instance.platformDispatcher.onPointerDataPacket!( ui.PointerDataPacket( data: [ _pointerData( ui.PointerChange.add, Offset.zero, pointer: 2, kind: PointerDeviceKind.touch, ), _pointerData( ui.PointerChange.down, const Offset(20, 10), pointer: 2, kind: PointerDeviceKind.touch, ), _pointerData( ui.PointerChange.cancel, const Offset(20, 10), pointer: 2, kind: PointerDeviceKind.touch, ), ], ), ); async.flushMicrotasks(); // The first pointer can still moving without crashing. RendererBinding.instance.platformDispatcher.onPointerDataPacket!( ui.PointerDataPacket( data: [ _pointerData( ui.PointerChange.add, Offset.zero, pointer: 1, kind: PointerDeviceKind.touch, ), _pointerData( ui.PointerChange.move, const Offset(10, 10), pointer: 1, kind: PointerDeviceKind.touch, ), _pointerData( ui.PointerChange.remove, const Offset(10, 10), pointer: 1, kind: PointerDeviceKind.touch, ), ], ), ); async.flushMicrotasks(); }); // Passes if no crashes. }); test('created callback is reset when controller is changed', () { final viewsController = FakeAndroidPlatformViewsController(); viewsController.registerViewType('webview'); final AndroidViewController firstController = PlatformViewsService.initAndroidView( id: 0, viewType: 'webview', layoutDirection: TextDirection.rtl, ); final renderBox = RenderAndroidView( viewController: firstController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{}, ); layout(renderBox); pumpFrame(phase: EnginePhase.flushSemantics); expect(firstController.createdCallbacks, isNotEmpty); expect(firstController.createdCallbacks.length, 1); final AndroidViewController secondController = PlatformViewsService.initAndroidView( id: 0, viewType: 'webview', layoutDirection: TextDirection.rtl, ); // Reset controller. renderBox.controller = secondController; expect(firstController.createdCallbacks, isEmpty); expect(secondController.createdCallbacks, isNotEmpty); expect(secondController.createdCallbacks.length, 1); }); test('render object changed its visual appearance after texture is created', () { FakeAsync().run((FakeAsync async) { final AndroidViewController viewController = PlatformViewsService.initAndroidView( id: 0, viewType: 'webview', layoutDirection: TextDirection.rtl, ); final renderBox = RenderAndroidView( viewController: viewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{}, ); final viewCreation = Completer(); const channel = MethodChannel('flutter/platform_views'); binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, ( MethodCall methodCall, ) async { assert(methodCall.method == 'create', 'Unexpected method call'); await viewCreation.future; return /*textureId=*/ 0; }); layout(renderBox); pumpFrame(phase: EnginePhase.paint); expect(renderBox.debugLayer, isNotNull); expect(renderBox.debugLayer!.hasChildren, isFalse); expect(viewController.isCreated, isFalse); expect(renderBox.debugNeedsPaint, isFalse); viewCreation.complete(); async.flushMicrotasks(); expect(viewController.isCreated, isTrue); expect(renderBox.debugNeedsPaint, isTrue); expect(renderBox.debugLayer!.hasChildren, isFalse); pumpFrame(phase: EnginePhase.paint); expect(renderBox.debugLayer!.hasChildren, isTrue); expect(renderBox.debugLayer!.firstChild, isA()); }); }); test('markNeedsPaint does not get called on a disposed RO', () async { FakeAsync().run((FakeAsync async) { final AndroidViewController viewController = PlatformViewsService.initAndroidView( id: 0, viewType: 'webview', layoutDirection: TextDirection.rtl, ); final renderBox = RenderAndroidView( viewController: viewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{}, ); final viewCreation = Completer(); const channel = MethodChannel('flutter/platform_views'); binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, ( MethodCall methodCall, ) async { assert(methodCall.method == 'create', 'Unexpected method call'); await viewCreation.future; return /*textureId=*/ 0; }); layout(renderBox); pumpFrame(phase: EnginePhase.paint); expect(renderBox.debugLayer, isNotNull); expect(renderBox.debugLayer!.hasChildren, isFalse); expect(viewController.isCreated, isFalse); expect(renderBox.debugNeedsPaint, isFalse); renderBox.dispose(); viewCreation.complete(); async.flushMicrotasks(); expect(viewController.isCreated, isTrue); expect(renderBox.debugNeedsPaint, isFalse); expect(renderBox.debugLayer, isNull); pumpFrame(phase: EnginePhase.paint); expect(renderBox.debugLayer, isNull); }); }); test('markNeedsPaint does not get called when setting the same viewController', () { FakeAsync().run((FakeAsync async) { final viewCreation = Completer(); const channel = MethodChannel('flutter/platform_views'); binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, ( MethodCall methodCall, ) async { assert(methodCall.method == 'create', 'Unexpected method call'); await viewCreation.future; return /*textureId=*/ 0; }); var futureCallbackRan = false; PlatformViewsService.initUiKitView( id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr, ).then((UiKitViewController viewController) { final renderBox = RenderUiKitView( viewController: viewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{}, ); layout(renderBox); pumpFrame(phase: EnginePhase.paint); expect(renderBox.debugNeedsPaint, isFalse); renderBox.viewController = viewController; expect(renderBox.debugNeedsPaint, isFalse); futureCallbackRan = true; }); viewCreation.complete(); async.flushMicrotasks(); expect(futureCallbackRan, true); }); }); group('RenderDarwinPlatformView', () { const channel = MethodChannel('flutter/platform_views'); late int gestureRejections; late Completer viewCreation; setUp(() { gestureRejections = 0; viewCreation = Completer(); binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, ( MethodCall methodCall, ) async { switch (methodCall.method) { case 'create': await viewCreation.future; case 'rejectGesture': gestureRejections++; default: throw UnsupportedError('Unexpected method call ${methodCall.method}.'); } return /*textureId=*/ 0; }); }); tearDown(() { binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); }); // Regression test for https://github.com/flutter/flutter/issues/83481. test('RenderUiKitView does not handle pointer events when not laid out', () async { await FakeAsync().run((FakeAsync async) { PlatformViewsService.initUiKitView( id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr, ).then((UiKitViewController viewController) { final renderBox = RenderUiKitView( viewController: viewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{}, ); renderBox.attach(TestRenderingFlutterBinding.instance.pipelineOwner); expect(renderBox.debugNeedsLayout, isTrue); expect(gestureRejections, 0); const event = PointerDownEvent(position: Offset(10, 10)); GestureBinding.instance.pointerRouter.route(event); // Didn't receive the gesture because the RenderBox is not laid out, // even though it's attached. expect(gestureRejections, 0); renderBox.detach(); }); viewCreation.complete(); async.flushMicrotasks(); }); }); test('RenderUiKitView handles pointer events when laid out', () async { await FakeAsync().run((FakeAsync async) { PlatformViewsService.initUiKitView( id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr, ).then((UiKitViewController viewController) { final renderBox = RenderUiKitView( viewController: viewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{}, ); expect(renderBox.debugNeedsLayout, isTrue); expect(gestureRejections, 0); const event = PointerDownEvent(position: Offset(10, 10)); GestureBinding.instance.pointerRouter.route(event); // Didn't receive the gesture because the RenderBox is not laid out. expect(gestureRejections, 0); layout(renderBox); pumpFrame(phase: EnginePhase.flushSemantics); expect(renderBox.debugNeedsLayout, isFalse); const event2 = PointerDownEvent(position: Offset(10, 10)); GestureBinding.instance.pointerRouter.route(event2); // Now that the RenderBox is laid out, received the gesture. expect(gestureRejections, 1); }); viewCreation.complete(); async.flushMicrotasks(); }); }); // Regression test for https://github.com/flutter/flutter/issues/83481. test('RenderAppKitView does not handle pointer events when not laid out', () async { await FakeAsync().run((FakeAsync async) { PlatformViewsService.initAppKitView( id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr, ).then((AppKitViewController viewController) { final renderBox = RenderAppKitView( viewController: viewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{}, ); renderBox.attach(TestRenderingFlutterBinding.instance.pipelineOwner); expect(renderBox.debugNeedsLayout, isTrue); expect(gestureRejections, 0); const event = PointerDownEvent(position: Offset(10, 10)); GestureBinding.instance.pointerRouter.route(event); // Didn't receive the gesture because the RenderBox is not laid out. expect(gestureRejections, 0); renderBox.detach(); }); viewCreation.complete(); async.flushMicrotasks(); }); }); // Regression test for https://github.com/flutter/flutter/issues/83481. test('RenderAppKitView handles pointer events when laid out', () async { await FakeAsync().run((FakeAsync async) { PlatformViewsService.initAppKitView( id: 0, viewType: 'webview', layoutDirection: TextDirection.ltr, ).then((AppKitViewController viewController) { final renderBox = RenderAppKitView( viewController: viewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{}, ); expect(renderBox.debugNeedsLayout, isTrue); expect(gestureRejections, 0); const event = PointerDownEvent(position: Offset(10, 10)); GestureBinding.instance.pointerRouter.route(event); // Didn't receive the gesture because the RenderBox is not laid out. expect(gestureRejections, 0); layout(renderBox); pumpFrame(phase: EnginePhase.flushSemantics); expect(renderBox.debugNeedsLayout, isFalse); const event2 = PointerDownEvent(position: Offset(10, 10)); GestureBinding.instance.pointerRouter.route(event2); // Now that the RenderBox is laid out, received the gesture. expect(gestureRejections, 1); }); viewCreation.complete(); async.flushMicrotasks(); }); }); }); test('PlatformViewRenderBox has transparent hitTestBehavior in semantics', () { final controller = FakePlatformViewController(0); final renderBox = PlatformViewRenderBox( controller: controller, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: >{}, ); final config = SemanticsConfiguration(); renderBox.describeSemanticsConfiguration(config); expect(config.hitTestBehavior, ui.SemanticsHitTestBehavior.transparent); expect(config.isSemanticBoundary, true); expect(config.platformViewId, 0); }); } ui.PointerData _pointerData( ui.PointerChange change, Offset logicalPosition, { int device = 0, PointerDeviceKind kind = PointerDeviceKind.mouse, int pointer = 0, }) { final double devicePixelRatio = RendererBinding.instance.platformDispatcher.implicitView!.devicePixelRatio; return ui.PointerData( pointerIdentifier: pointer, embedderId: pointer, change: change, physicalX: logicalPosition.dx * devicePixelRatio, physicalY: logicalPosition.dy * devicePixelRatio, kind: kind, device: device, ); } class _EmptyPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = const Color(0x00000000); canvas.drawRect(Offset.zero & size, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }