| // 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 'package:flutter/gestures.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'fake_platform_views.dart'; |
| |
| void main() { |
| final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
| |
| group('Android', () { |
| late FakeAndroidPlatformViewsController viewsController; |
| setUp(() { |
| viewsController = FakeAndroidPlatformViewsController(); |
| }); |
| |
| test('create Android view of unregistered type', () async { |
| await expectLater( |
| () => PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'web', |
| layoutDirection: TextDirection.ltr, |
| ).create(size: const Size(100.0, 100.0)), |
| throwsA(isA<PlatformException>()), |
| ); |
| viewsController.registerViewType('web'); |
| |
| try { |
| await PlatformViewsService.initSurfaceAndroidView( |
| id: 0, |
| viewType: 'web', |
| layoutDirection: TextDirection.ltr, |
| ).create(size: const Size(1.0, 1.0)); |
| } catch (e) { |
| expect(false, isTrue, reason: 'did not expected any exception, but instead got `$e`'); |
| } |
| |
| try { |
| await PlatformViewsService.initAndroidView( |
| id: 1, |
| viewType: 'web', |
| layoutDirection: TextDirection.ltr, |
| ).create(size: const Size(1.0, 1.0)); |
| } catch (e) { |
| expect(false, isTrue, reason: 'did not expected any exception, but instead got `$e`'); |
| } |
| }); |
| |
| test('create VD-fallback Android views', () async { |
| viewsController.registerViewType('webview'); |
| await PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ).create(size: const Size(100.0, 100.0)); |
| await PlatformViewsService.initAndroidView( |
| id: 1, |
| viewType: 'webview', |
| layoutDirection: TextDirection.rtl, |
| ).create(size: const Size(200.0, 300.0)); |
| expect( |
| viewsController.views, |
| unorderedEquals(<FakeAndroidPlatformView>[ |
| const FakeAndroidPlatformView( |
| 0, |
| 'webview', |
| Size(100.0, 100.0), |
| AndroidViewController.kAndroidLayoutDirectionLtr, |
| ), |
| const FakeAndroidPlatformView( |
| 1, |
| 'webview', |
| Size(200.0, 300.0), |
| AndroidViewController.kAndroidLayoutDirectionRtl, |
| ), |
| ]), |
| ); |
| }); |
| |
| test('create HC-fallback Android views', () async { |
| viewsController.registerViewType('webview'); |
| await PlatformViewsService.initSurfaceAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ).create(size: const Size(100.0, 100.0)); |
| await PlatformViewsService.initSurfaceAndroidView( |
| id: 1, |
| viewType: 'webview', |
| layoutDirection: TextDirection.rtl, |
| ).create(size: const Size(200.0, 300.0)); |
| expect( |
| viewsController.views, |
| unorderedEquals(<FakeAndroidPlatformView>[ |
| const FakeAndroidPlatformView( |
| 0, |
| 'webview', |
| Size(100.0, 100.0), |
| AndroidViewController.kAndroidLayoutDirectionLtr, |
| hybridFallback: true, |
| ), |
| const FakeAndroidPlatformView( |
| 1, |
| 'webview', |
| Size(200.0, 300.0), |
| AndroidViewController.kAndroidLayoutDirectionRtl, |
| hybridFallback: true, |
| ), |
| ]), |
| ); |
| }); |
| |
| test('create HC-only Android views', () async { |
| viewsController.registerViewType('webview'); |
| await PlatformViewsService.initExpensiveAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ).create(size: const Size(100.0, 100.0)); |
| await PlatformViewsService.initExpensiveAndroidView( |
| id: 1, |
| viewType: 'webview', |
| layoutDirection: TextDirection.rtl, |
| ).create(size: const Size(200.0, 300.0)); |
| expect( |
| viewsController.views, |
| unorderedEquals(<FakeAndroidPlatformView>[ |
| const FakeAndroidPlatformView( |
| 0, |
| 'webview', |
| null, |
| AndroidViewController.kAndroidLayoutDirectionLtr, |
| hybrid: true, |
| ), |
| const FakeAndroidPlatformView( |
| 1, |
| 'webview', |
| null, |
| AndroidViewController.kAndroidLayoutDirectionRtl, |
| hybrid: true, |
| ), |
| ]), |
| ); |
| }); |
| |
| test('default view does not use view composition by default', () async { |
| viewsController.registerViewType('webview'); |
| final AndroidViewController controller = PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await controller.create(size: const Size(100.0, 100.0)); |
| expect(controller.requiresViewComposition, false); |
| }); |
| |
| test('default view does not use view composition in fallback mode', () async { |
| viewsController.registerViewType('webview'); |
| viewsController.allowTextureLayerMode = false; |
| final AndroidViewController controller = PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await controller.create(size: const Size(100.0, 100.0)); |
| viewsController.allowTextureLayerMode = true; |
| expect(controller.requiresViewComposition, false); |
| }); |
| |
| test('surface view does not use view composition by default', () async { |
| viewsController.registerViewType('webview'); |
| final AndroidViewController controller = PlatformViewsService.initSurfaceAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await controller.create(size: const Size(100.0, 100.0)); |
| expect(controller.requiresViewComposition, false); |
| }); |
| |
| test('surface view does uses view composition in fallback mode', () async { |
| viewsController.registerViewType('webview'); |
| viewsController.allowTextureLayerMode = false; |
| final AndroidViewController controller = PlatformViewsService.initSurfaceAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await controller.create(size: const Size(100.0, 100.0)); |
| viewsController.allowTextureLayerMode = true; |
| expect(controller.requiresViewComposition, true); |
| }); |
| |
| test('expensive view uses view composition', () async { |
| viewsController.registerViewType('webview'); |
| final AndroidViewController controller = PlatformViewsService.initExpensiveAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await controller.create(size: const Size(100.0, 100.0)); |
| expect(controller.requiresViewComposition, true); |
| }); |
| |
| test('reuse Android view id', () async { |
| viewsController.registerViewType('webview'); |
| final AndroidViewController controller = PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await controller.create(size: const Size(100.0, 100.0)); |
| await expectLater(() { |
| final AndroidViewController controller = PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'web', |
| layoutDirection: TextDirection.ltr, |
| ); |
| return controller.create(size: const Size(100.0, 100.0)); |
| }, throwsA(isA<PlatformException>())); |
| }); |
| |
| test('dispose Android view', () async { |
| viewsController.registerViewType('webview'); |
| await PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ).create(size: const Size(100.0, 100.0)); |
| |
| final AndroidViewController viewController = PlatformViewsService.initAndroidView( |
| id: 1, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await viewController.create(size: const Size(200.0, 300.0)); |
| await viewController.dispose(); |
| |
| final AndroidViewController surfaceViewController = |
| PlatformViewsService.initSurfaceAndroidView( |
| id: 1, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await surfaceViewController.create(size: const Size(200.0, 300.0)); |
| await surfaceViewController.dispose(); |
| |
| expect( |
| viewsController.views, |
| unorderedEquals(<FakeAndroidPlatformView>[ |
| const FakeAndroidPlatformView( |
| 0, |
| 'webview', |
| Size(100.0, 100.0), |
| AndroidViewController.kAndroidLayoutDirectionLtr, |
| ), |
| ]), |
| ); |
| }); |
| |
| test('dispose Android view twice', () async { |
| viewsController.registerViewType('webview'); |
| final AndroidViewController viewController = PlatformViewsService.initAndroidView( |
| id: 1, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await viewController.create(size: const Size(200.0, 300.0)); |
| await viewController.dispose(); |
| await viewController.dispose(); |
| }); |
| |
| test('dispose clears focusCallbacks', () async { |
| var didFocus = false; |
| viewsController.registerViewType('webview'); |
| final AndroidViewController viewController = PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| onFocus: () { |
| didFocus = true; |
| }, |
| ); |
| await viewController.create(size: const Size(100.0, 100.0)); |
| await viewController.dispose(); |
| final ByteData message = SystemChannels.platform_views.codec.encodeMethodCall( |
| const MethodCall('viewFocused', 0), |
| ); |
| await binding.defaultBinaryMessenger.handlePlatformMessage( |
| SystemChannels.platform_views.name, |
| message, |
| (_) {}, |
| ); |
| expect(didFocus, isFalse); |
| }); |
| |
| test('resize Android view', () async { |
| viewsController.registerViewType('webview'); |
| await PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ).create(size: const Size(100.0, 100.0)); |
| |
| final AndroidViewController androidView = PlatformViewsService.initAndroidView( |
| id: 1, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await androidView.create(size: const Size(200.0, 300.0)); |
| await androidView.setSize(const Size(500.0, 500.0)); |
| |
| expect( |
| viewsController.views, |
| unorderedEquals(<FakeAndroidPlatformView>[ |
| const FakeAndroidPlatformView( |
| 0, |
| 'webview', |
| Size(100.0, 100.0), |
| AndroidViewController.kAndroidLayoutDirectionLtr, |
| ), |
| const FakeAndroidPlatformView( |
| 1, |
| 'webview', |
| Size(500.0, 500.0), |
| AndroidViewController.kAndroidLayoutDirectionLtr, |
| ), |
| ]), |
| ); |
| }); |
| |
| test('OnPlatformViewCreated callback', () async { |
| viewsController.registerViewType('webview'); |
| final createdViews = <int>[]; |
| void callback(int id) { |
| createdViews.add(id); |
| } |
| |
| final AndroidViewController controller1 = PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| )..addOnPlatformViewCreatedListener(callback); |
| expect(createdViews, isEmpty); |
| |
| await controller1.create(size: const Size(100.0, 100.0)); |
| expect(createdViews, orderedEquals(<int>[0])); |
| |
| final AndroidViewController controller2 = PlatformViewsService.initAndroidView( |
| id: 5, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| )..addOnPlatformViewCreatedListener(callback); |
| expect(createdViews, orderedEquals(<int>[0])); |
| |
| await controller2.create(size: const Size(100.0, 200.0)); |
| expect(createdViews, orderedEquals(<int>[0, 5])); |
| |
| final AndroidViewController controller3 = PlatformViewsService.initAndroidView( |
| id: 10, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| )..addOnPlatformViewCreatedListener(callback); |
| expect(createdViews, orderedEquals(<int>[0, 5])); |
| |
| await Future.wait(<Future<void>>[ |
| controller3.create(size: const Size(100.0, 200.0)), |
| controller3.dispose(), |
| ]); |
| |
| expect(createdViews, orderedEquals(<int>[0, 5])); |
| }); |
| |
| test("change Android view's directionality before creation", () async { |
| viewsController.registerViewType('webview'); |
| final AndroidViewController viewController = PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.rtl, |
| ); |
| await viewController.setLayoutDirection(TextDirection.ltr); |
| await viewController.create(size: const Size(100.0, 100.0)); |
| expect( |
| viewsController.views, |
| unorderedEquals(<FakeAndroidPlatformView>[ |
| const FakeAndroidPlatformView( |
| 0, |
| 'webview', |
| Size(100.0, 100.0), |
| AndroidViewController.kAndroidLayoutDirectionLtr, |
| ), |
| ]), |
| ); |
| }); |
| |
| test("change Android view's directionality after creation", () async { |
| viewsController.registerViewType('webview'); |
| final AndroidViewController viewController = PlatformViewsService.initAndroidView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await viewController.setLayoutDirection(TextDirection.rtl); |
| await viewController.create(size: const Size(100.0, 100.0)); |
| expect( |
| viewsController.views, |
| unorderedEquals(<FakeAndroidPlatformView>[ |
| const FakeAndroidPlatformView( |
| 0, |
| 'webview', |
| Size(100.0, 100.0), |
| AndroidViewController.kAndroidLayoutDirectionRtl, |
| ), |
| ]), |
| ); |
| }); |
| |
| test("set Android view's offset if view is created", () async { |
| viewsController.registerViewType('webview'); |
| final AndroidViewController viewController = PlatformViewsService.initAndroidView( |
| id: 7, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await viewController.create(size: const Size(100.0, 100.0)); |
| await viewController.setOffset(const Offset(10, 20)); |
| expect(viewsController.offsets, equals(<int, Offset>{7: const Offset(10, 20)})); |
| }); |
| |
| test("doesn't set Android view's offset if view isn't created", () async { |
| viewsController.registerViewType('webview'); |
| final AndroidViewController viewController = PlatformViewsService.initAndroidView( |
| id: 7, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await viewController.setOffset(const Offset(10, 20)); |
| expect(viewsController.offsets, equals(<int, Offset>{})); |
| }); |
| |
| testWidgets('motion event converter does not duplicate move events', ( |
| WidgetTester tester, |
| ) async { |
| final log = <MethodCall>[]; |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( |
| SystemChannels.platform_views, |
| (MethodCall methodCall) async { |
| log.add(methodCall); |
| return null; |
| }, |
| ); |
| |
| final AndroidViewController viewController = PlatformViewsService.initSurfaceAndroidView( |
| id: 7, |
| viewType: 'web', |
| layoutDirection: TextDirection.ltr, |
| ); |
| viewController.pointTransformer = (Offset offset) => offset; |
| |
| const pointerCount = 10; |
| for (var i = 0; i < pointerCount; i++) { |
| final PointerEvent event = PointerDownEvent( |
| timeStamp: const Duration(milliseconds: 1), |
| pointer: i, |
| ); |
| viewController.dispatchPointerEvent(event); |
| } |
| |
| // Pointer event platform data constant from _AndroidMotionEventConverter |
| const kPointerDataFlagMultiple = 2; |
| |
| for (var i = 0; i < pointerCount; i++) { |
| final PointerEvent event = PointerMoveEvent( |
| timeStamp: const Duration(milliseconds: 2), |
| pointer: i, |
| platformData: kPointerDataFlagMultiple | (pointerCount << 8), |
| ); |
| viewController.dispatchPointerEvent(event); |
| } |
| |
| // Indexes in the list returned by AndroidMotionEvent._asList |
| const kAndroidMotionEventListIndexAction = 3; |
| const kAndroidMotionEventListIndexPointerCount = 4; |
| |
| final List<MethodCall> moveCalls = log.where((MethodCall call) { |
| final args = call.arguments as List<dynamic>; |
| return call.method == 'touch' && |
| args[kAndroidMotionEventListIndexAction] == AndroidViewController.kActionMove; |
| }).toList(); |
| |
| // The _AndroidMotionEventConverter should yield one touch event containing all of the pointers. |
| expect(moveCalls.length, equals(1)); |
| final moveArgs = moveCalls.single.arguments as List<dynamic>; |
| expect(moveArgs[kAndroidMotionEventListIndexPointerCount], equals(pointerCount)); |
| }); |
| }); |
| |
| group('iOS', () { |
| late FakeIosPlatformViewsController viewsController; |
| setUp(() { |
| viewsController = FakeIosPlatformViewsController(); |
| }); |
| |
| test('create iOS view of unregistered type', () async { |
| expect(() { |
| return PlatformViewsService.initUiKitView( |
| id: 0, |
| viewType: 'web', |
| layoutDirection: TextDirection.ltr, |
| ); |
| }, throwsA(isA<PlatformException>())); |
| }); |
| |
| test('create iOS views', () async { |
| viewsController.registerViewType('webview'); |
| await PlatformViewsService.initUiKitView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await PlatformViewsService.initUiKitView( |
| id: 1, |
| viewType: 'webview', |
| layoutDirection: TextDirection.rtl, |
| ); |
| expect( |
| viewsController.views, |
| unorderedEquals(<FakeUiKitView>[ |
| const FakeUiKitView(0, 'webview'), |
| const FakeUiKitView(1, 'webview'), |
| ]), |
| ); |
| }); |
| |
| test('reuse iOS view id', () async { |
| viewsController.registerViewType('webview'); |
| await PlatformViewsService.initUiKitView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| expect( |
| () => PlatformViewsService.initUiKitView( |
| id: 0, |
| viewType: 'web', |
| layoutDirection: TextDirection.ltr, |
| ), |
| throwsA(isA<PlatformException>()), |
| ); |
| }); |
| |
| test('dispose iOS view', () async { |
| viewsController.registerViewType('webview'); |
| await PlatformViewsService.initUiKitView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| final UiKitViewController viewController = await PlatformViewsService.initUiKitView( |
| id: 1, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| |
| viewController.dispose(); |
| expect( |
| viewsController.views, |
| unorderedEquals(<FakeUiKitView>[const FakeUiKitView(0, 'webview')]), |
| ); |
| }); |
| |
| test('dispose inexisting iOS view', () async { |
| viewsController.registerViewType('webview'); |
| await PlatformViewsService.initUiKitView( |
| id: 0, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| final UiKitViewController viewController = await PlatformViewsService.initUiKitView( |
| id: 1, |
| viewType: 'webview', |
| layoutDirection: TextDirection.ltr, |
| ); |
| await viewController.dispose(); |
| expect(() async { |
| await viewController.dispose(); |
| }, throwsA(isA<PlatformException>())); |
| }); |
| }); |
| |
| test('toString works as intended', () async { |
| const androidPointerProperties = AndroidPointerProperties(id: 0, toolType: 0); |
| expect(androidPointerProperties.toString(), 'AndroidPointerProperties(id: 0, toolType: 0)'); |
| |
| const zero = 0.0; |
| const androidPointerCoords = AndroidPointerCoords( |
| orientation: zero, |
| pressure: zero, |
| size: zero, |
| toolMajor: zero, |
| toolMinor: zero, |
| touchMajor: zero, |
| touchMinor: zero, |
| x: zero, |
| y: zero, |
| ); |
| expect( |
| androidPointerCoords.toString(), |
| 'AndroidPointerCoords(orientation: $zero, ' |
| 'pressure: $zero, ' |
| 'size: $zero, ' |
| 'toolMajor: $zero, ' |
| 'toolMinor: $zero, ' |
| 'touchMajor: $zero, ' |
| 'touchMinor: $zero, ' |
| 'x: $zero, ' |
| 'y: $zero)', |
| ); |
| |
| final androidMotionEvent = AndroidMotionEvent( |
| downTime: 0, |
| eventTime: 0, |
| action: 0, |
| pointerCount: 0, |
| pointerProperties: <AndroidPointerProperties>[], |
| pointerCoords: <AndroidPointerCoords>[], |
| metaState: 0, |
| buttonState: 0, |
| xPrecision: zero, |
| yPrecision: zero, |
| deviceId: 0, |
| edgeFlags: 0, |
| source: 0, |
| flags: 0, |
| motionEventId: 0, |
| ); |
| expect( |
| androidMotionEvent.toString(), |
| 'AndroidPointerEvent(downTime: 0, ' |
| 'eventTime: 0, ' |
| 'action: 0, ' |
| 'pointerCount: 0, ' |
| 'pointerProperties: [], ' |
| 'pointerCoords: [], ' |
| 'metaState: 0, ' |
| 'buttonState: 0, ' |
| 'xPrecision: $zero, ' |
| 'yPrecision: $zero, ' |
| 'deviceId: 0, ' |
| 'edgeFlags: 0, ' |
| 'source: 0, ' |
| 'flags: 0, ' |
| 'motionEventId: 0)', |
| ); |
| }); |
| } |