diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index c7ac2e0200f9..2bd987236f24 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -24,8 +24,10 @@ dependencies: webview_flutter_wkwebview: ^2.2.0 dev_dependencies: + build_runner: ^2.1.5 flutter_driver: sdk: flutter flutter_test: sdk: flutter + mockito: ^5.0.16 pedantic: ^1.10.0 diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index e325f68dc0b7..69d4142eb150 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -2,37 +2,47 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; import 'package:flutter/src/foundation/basic_types.dart'; import 'package:flutter/src/gestures/recognizer.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'webview_flutter_test.mocks.dart'; + typedef void VoidCallback(); +@GenerateMocks([WebViewPlatform, WebViewPlatformController]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final _FakePlatformViewsController fakePlatformViewsController = - _FakePlatformViewsController(); - - final _FakeCookieManager _fakeCookieManager = _FakeCookieManager(); - - setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); - SystemChannels.platform - .setMockMethodCallHandler(_fakeCookieManager.onMethodCall); - }); + late MockWebViewPlatform mockWebViewPlatform; + late MockWebViewPlatformController mockWebViewPlatformController; setUp(() { - fakePlatformViewsController.reset(); - _fakeCookieManager.reset(); + mockWebViewPlatformController = MockWebViewPlatformController(); + mockWebViewPlatform = MockWebViewPlatform(); + when(mockWebViewPlatform.build( + context: anyNamed('context'), + creationParams: anyNamed('creationParams'), + webViewPlatformCallbacksHandler: + anyNamed('webViewPlatformCallbacksHandler'), + javascriptChannelRegistry: anyNamed('javascriptChannelRegistry'), + onWebViewPlatformCreated: anyNamed('onWebViewPlatformCreated'), + gestureRecognizers: anyNamed('gestureRecognizers'), + )).thenAnswer((Invocation invocation) { + final WebViewPlatformCreatedCallback onWebViewPlatformCreated = + invocation.namedArguments[Symbol('onWebViewPlatformCreated')]; + return TestPlatformWebView( + mockWebViewPlatformController: mockWebViewPlatformController, + onWebViewPlatformCreated: onWebViewPlatformCreated, + ); + }); + + WebView.platform = mockWebViewPlatform; }); testWidgets('Create WebView', (WidgetTester tester) async { @@ -40,35 +50,41 @@ void main() { }); testWidgets('Initial url', (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); + await tester.pumpWidget(WebView(initialUrl: 'https://youtube.com')); - expect(await controller.currentUrl(), 'https://youtube.com'); + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; + + expect(params.initialUrl, 'https://youtube.com'); }); testWidgets('Javascript mode', (WidgetTester tester) async { await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', javascriptMode: JavascriptMode.unrestricted, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams unrestrictedparams = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.javascriptMode, JavascriptMode.unrestricted); + expect( + unrestrictedparams.webSettings!.javascriptMode, + JavascriptMode.unrestricted, + ); await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', javascriptMode: JavascriptMode.disabled, )); - expect(platformWebView.javascriptMode, JavascriptMode.disabled); + + final CreationParams disabledparams = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; + + expect(disabledparams.webSettings!.javascriptMode, JavascriptMode.disabled); }); testWidgets('Load url', (WidgetTester tester) async { @@ -85,7 +101,10 @@ void main() { await controller!.loadUrl('https://flutter.io'); - expect(await controller!.currentUrl(), 'https://flutter.io'); + verify(mockWebViewPlatformController.loadUrl( + 'https://flutter.io', + argThat(isNull), + )); }); testWidgets('Invalid urls', (WidgetTester tester) async { @@ -100,14 +119,15 @@ void main() { expect(controller, isNotNull); - expect(await controller!.currentUrl(), isNull); + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(() => controller!.loadUrl(''), throwsA(anything)); - expect(await controller!.currentUrl(), isNull); + expect(params.initialUrl, isNull); - // Missing schema. + expect(() => controller!.loadUrl(''), throwsA(anything)); expect(() => controller!.loadUrl('flutter.io'), throwsA(anything)); - expect(await controller!.currentUrl(), isNull); }); testWidgets('Headers in loadUrl', (WidgetTester tester) async { @@ -126,25 +146,11 @@ void main() { 'CACHE-CONTROL': 'ABC' }; await controller!.loadUrl('https://flutter.io', headers: headers); - expect(await controller!.currentUrl(), equals('https://flutter.io')); - }); - testWidgets("Can't go back before loading a page", - (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final bool canGoBackNoPageLoaded = await controller!.canGoBack(); - - expect(canGoBackNoPageLoaded, false); + verify(mockWebViewPlatformController.loadUrl( + 'https://flutter.io', + {'CACHE-CONTROL': 'ABC'}, + )); }); testWidgets("Clear Cache", (WidgetTester tester) async { @@ -158,68 +164,16 @@ void main() { ); expect(controller, isNotNull); - expect(fakePlatformViewsController.lastCreatedView!.hasCache, true); await controller!.clearCache(); - expect(fakePlatformViewsController.lastCreatedView!.hasCache, false); - }); - - testWidgets("Can't go back with no history", (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - final bool canGoBackFirstPageLoaded = await controller!.canGoBack(); - - expect(canGoBackFirstPageLoaded, false); + verify(mockWebViewPlatformController.clearCache()); }); testWidgets('Can go back', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.loadUrl('https://www.google.com'); - final bool canGoBackSecondPageLoaded = await controller!.canGoBack(); - - expect(canGoBackSecondPageLoaded, true); - }); - - testWidgets("Can't go forward before loading a page", - (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final bool canGoForwardNoPageLoaded = await controller!.canGoForward(); + when(mockWebViewPlatformController.canGoBack()) + .thenAnswer((_) => Future.value(true)); - expect(canGoForwardNoPageLoaded, false); - }); - - testWidgets("Can't go forward with no history", (WidgetTester tester) async { WebViewController? controller; await tester.pumpWidget( WebView( @@ -231,16 +185,16 @@ void main() { ); expect(controller, isNotNull); - final bool canGoForwardFirstPageLoaded = await controller!.canGoForward(); - - expect(canGoForwardFirstPageLoaded, false); + expect(controller!.canGoBack(), completion(true)); }); - testWidgets('Can go forward', (WidgetTester tester) async { + testWidgets("Can't go forward", (WidgetTester tester) async { + when(mockWebViewPlatformController.canGoForward()) + .thenAnswer((_) => Future.value(false)); + WebViewController? controller; await tester.pumpWidget( WebView( - initialUrl: 'https://flutter.io', onWebViewCreated: (WebViewController webViewController) { controller = webViewController; }, @@ -248,12 +202,7 @@ void main() { ); expect(controller, isNotNull); - - await controller!.loadUrl('https://youtube.com'); - await controller!.goBack(); - final bool canGoForwardFirstPageBacked = await controller!.canGoForward(); - - expect(canGoForwardFirstPageBacked, true); + expect(controller!.canGoForward(), completion(false)); }); testWidgets('Go back', (WidgetTester tester) async { @@ -267,17 +216,8 @@ void main() { ), ); - expect(controller, isNotNull); - - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - - expect(await controller!.currentUrl(), 'https://flutter.io'); - await controller!.goBack(); - - expect(await controller!.currentUrl(), 'https://youtube.com'); + verify(mockWebViewPlatformController.goBack()); }); testWidgets('Go forward', (WidgetTester tester) async { @@ -292,23 +232,14 @@ void main() { ); expect(controller, isNotNull); - - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - - expect(await controller!.currentUrl(), 'https://flutter.io'); - - await controller!.goBack(); - - expect(await controller!.currentUrl(), 'https://youtube.com'); - await controller!.goForward(); - - expect(await controller!.currentUrl(), 'https://flutter.io'); + verify(mockWebViewPlatformController.goForward()); }); testWidgets('Current URL', (WidgetTester tester) async { + when(mockWebViewPlatformController.currentUrl()) + .thenAnswer((_) => Future.value('https://youtube.com')); + WebViewController? controller; await tester.pumpWidget( WebView( @@ -319,17 +250,6 @@ void main() { ); expect(controller, isNotNull); - - // Test a WebView without an explicitly set first URL. - expect(await controller!.currentUrl(), isNull); - - await controller!.loadUrl('https://youtube.com'); - expect(await controller!.currentUrl(), 'https://youtube.com'); - - await controller!.loadUrl('https://flutter.io'); - expect(await controller!.currentUrl(), 'https://flutter.io'); - - await controller!.goBack(); expect(await controller!.currentUrl(), 'https://youtube.com'); }); @@ -344,23 +264,14 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - expect(platformWebView.currentUrl, 'https://flutter.io'); - expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); - await controller.reload(); - - expect(platformWebView.currentUrl, 'https://flutter.io'); - expect(platformWebView.amountOfReloadsOnCurrentUrl, 1); - - await controller.loadUrl('https://youtube.com'); - - expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); + verify(mockWebViewPlatformController.reload()); }); testWidgets('evaluate Javascript', (WidgetTester tester) async { + when(mockWebViewPlatformController.evaluateJavascript('fake js string')) + .thenAnswer((_) => Future.value('fake js string')); + late WebViewController controller; await tester.pumpWidget( WebView( @@ -371,10 +282,11 @@ void main() { }, ), ); + expect( // ignore: deprecated_member_use_from_same_package - await controller.evaluateJavascript("fake js string"), - "fake js string", + await controller.evaluateJavascript('fake js string'), + 'fake js string', reason: 'should get the argument'); }); @@ -409,8 +321,7 @@ void main() { ), ); await controller.runJavascript('fake js string'); - expect(fakePlatformViewsController.lastCreatedView?.lastRunJavaScriptString, - 'fake js string'); + verify(mockWebViewPlatformController.runJavascript('fake js string')); }); testWidgets('runJavaScript with JavascriptMode disabled', @@ -432,6 +343,10 @@ void main() { }); testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + when(mockWebViewPlatformController + .runJavascriptReturningResult('fake js string')) + .thenAnswer((_) => Future.value('fake js string')); + late WebViewController controller; await tester.pumpWidget( WebView( @@ -442,8 +357,8 @@ void main() { }, ), ); - expect(await controller.runJavascriptReturningResult("fake js string"), - "fake js string", + expect(await controller.runJavascriptReturningResult('fake js string'), + 'fake js string', reason: 'should get the argument'); }); @@ -466,18 +381,9 @@ void main() { }); testWidgets('Cookies can be cleared once', (WidgetTester tester) async { - await tester.pumpWidget( - const WebView( - initialUrl: 'https://flutter.io', - ), - ); - final CookieManager cookieManager = CookieManager(); - final bool hasCookies = await cookieManager.clearCookies(); - expect(hasCookies, true); - }); + when(mockWebViewPlatform.clearCookies()) + .thenAnswer((_) => Future.value(true)); - testWidgets('Second cookie clear does not have cookies', - (WidgetTester tester) async { await tester.pumpWidget( const WebView( initialUrl: 'https://flutter.io', @@ -486,8 +392,6 @@ void main() { final CookieManager cookieManager = CookieManager(); final bool hasCookies = await cookieManager.clearCookies(); expect(hasCookies, true); - final bool hasCookiesSecond = await cookieManager.clearCookies(); - expect(hasCookiesSecond, false); }); testWidgets('Initial JavaScript channels', (WidgetTester tester) async { @@ -503,10 +407,12 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.javascriptChannelNames, + expect(params.javascriptChannelNames, unorderedEquals(['Tts', 'Alarm'])); }); @@ -570,11 +476,15 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).first; - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); + expect( + channelRegistry.channels.keys, + unorderedEquals(['Tts', 'Alarm2', 'Alarm3']), + ); }); testWidgets('Remove all JavaScript channels and then add', @@ -609,11 +519,12 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).last; - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts'])); + expect(channelRegistry.channels.keys, unorderedEquals(['Tts'])); }); testWidgets('JavaScript channel messages', (WidgetTester tester) async { @@ -637,14 +548,16 @@ void main() { ), ); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).single; expect(ttsMessagesReceived, isEmpty); expect(alarmMessagesReceived, isEmpty); - platformWebView.fakeJavascriptPostMessage('Tts', 'Hello'); - platformWebView.fakeJavascriptPostMessage('Tts', 'World'); + channelRegistry.onJavascriptChannelMessage('Tts', 'Hello'); + channelRegistry.onJavascriptChannelMessage('Tts', 'World'); expect(ttsMessagesReceived, ['Hello', 'World']); }); @@ -660,12 +573,14 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; - platformWebView.fakeOnPageStartedCallback(); + handler.onPageStarted('https://youtube.com'); - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); testWidgets('onPageStarted is null', (WidgetTester tester) async { @@ -674,12 +589,14 @@ void main() { onPageStarted: null, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; // The platform side will always invoke a call for onPageStarted. This is // to test that it does not crash on a null callback. - platformWebView.fakeOnPageStartedCallback(); + handler.onPageStarted('https://youtube.com'); }); testWidgets('onPageStarted changed', (WidgetTester tester) async { @@ -697,12 +614,13 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageStartedCallback(); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last; + handler.onPageStarted('https://youtube.com'); - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); }); @@ -717,12 +635,13 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; + handler.onPageFinished('https://youtube.com'); - platformWebView.fakeOnPageFinishedCallback(); - - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); testWidgets('onPageFinished is null', (WidgetTester tester) async { @@ -731,12 +650,13 @@ void main() { onPageFinished: null, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; // The platform side will always invoke a call for onPageFinished. This is // to test that it does not crash on a null callback. - platformWebView.fakeOnPageFinishedCallback(); + handler.onPageFinished('https://youtube.com'); }); testWidgets('onPageFinished changed', (WidgetTester tester) async { @@ -754,12 +674,13 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnPageFinishedCallback(); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last; + handler.onPageFinished('https://youtube.com'); - expect(platformWebView.currentUrl, returnedUrl); + expect(returnedUrl, 'https://youtube.com'); }); }); @@ -774,10 +695,11 @@ void main() { }, )); - final FakePlatformWebView? platformWebView = - fakePlatformViewsController.lastCreatedView; - - platformWebView?.fakeOnProgressCallback(50); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; + handler.onProgress(50); expect(loadingProgress, 50); }); @@ -788,11 +710,13 @@ void main() { onProgress: null, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single; // This is to test that it does not crash on a null callback. - platformWebView.fakeOnProgressCallback(50); + handler.onProgress(50); }); testWidgets('onLoadingProgress changed', (WidgetTester tester) async { @@ -810,10 +734,11 @@ void main() { }, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - - platformWebView.fakeOnProgressCallback(50); + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last; + handler.onProgress(50); expect(loadingProgress, 50); }); @@ -825,10 +750,12 @@ void main() { initialUrl: 'https://youtube.com', )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.hasNavigationDelegate, false); + expect(params.webSettings!.hasNavigationDelegate, false); await tester.pumpWidget(WebView( initialUrl: 'https://youtube.com', @@ -836,7 +763,12 @@ void main() { NavigationDecision.navigate, )); - expect(platformWebView.hasNavigationDelegate, true); + final WebSettings updateSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .single; + + expect(updateSettings.hasNavigationDelegate, true); }); testWidgets('Block navigation', (WidgetTester tester) async { @@ -852,22 +784,38 @@ void main() { : NavigationDecision.prevent; })); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final List args = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + webViewPlatformCallbacksHandler: true, + ); + + final CreationParams params = args[0]; + expect(params.webSettings!.hasNavigationDelegate, true); - expect(platformWebView.hasNavigationDelegate, true); + final WebViewPlatformCallbacksHandler handler = args[1]; - platformWebView.fakeNavigate('https://www.google.com'); // The navigation delegate only allows navigation to https://flutter.dev // so we should still be in https://youtube.com. - expect(platformWebView.currentUrl, 'https://youtube.com'); + expect( + handler.onNavigationRequest( + url: 'https://www.google.com', + isForMainFrame: true, + ), + completion(false), + ); + expect(navigationRequests.length, 1); expect(navigationRequests[0].url, 'https://www.google.com'); expect(navigationRequests[0].isForMainFrame, true); - platformWebView.fakeNavigate('https://flutter.dev'); - await tester.pump(); - expect(platformWebView.currentUrl, 'https://flutter.dev'); + expect( + handler.onNavigationRequest( + url: 'https://flutter.dev', + isForMainFrame: true, + ), + completion(true), + ); }); }); @@ -877,41 +825,50 @@ void main() { debuggingEnabled: true, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.debuggingEnabled, true); + expect(params.webSettings!.debuggingEnabled, true); }); testWidgets('defaults to false', (WidgetTester tester) async { await tester.pumpWidget(const WebView()); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.debuggingEnabled, false); + expect(params.webSettings!.debuggingEnabled, false); }); testWidgets('can be changed', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(WebView(key: key)); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - await tester.pumpWidget(WebView( key: key, debuggingEnabled: true, )); - expect(platformWebView.debuggingEnabled, true); + final WebSettings enabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last; + expect(enabledSettings.debuggingEnabled, true); await tester.pumpWidget(WebView( key: key, debuggingEnabled: false, )); - expect(platformWebView.debuggingEnabled, false); + final WebSettings disabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last; + expect(disabledSettings.debuggingEnabled, false); }); }); @@ -921,46 +878,56 @@ void main() { zoomEnabled: true, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.zoomEnabled, isTrue); + expect(params.webSettings!.zoomEnabled, isTrue); }); testWidgets('defaults to true', (WidgetTester tester) async { await tester.pumpWidget(const WebView()); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.zoomEnabled, isTrue); + expect(params.webSettings!.zoomEnabled, isTrue); }); testWidgets('can be changed', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(WebView(key: key)); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; - await tester.pumpWidget(WebView( key: key, zoomEnabled: true, )); - expect(platformWebView.zoomEnabled, isTrue); + final WebSettings enabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last; + // Zoom defaults to true, so no changes are made to settings. + expect(enabledSettings.zoomEnabled, isNull); await tester.pumpWidget(WebView( key: key, zoomEnabled: false, )); - expect(platformWebView.zoomEnabled, isFalse); + final WebSettings disabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last; + expect(disabledSettings.zoomEnabled, isFalse); }); }); group('Custom platform implementation', () { - setUpAll(() { + setUp(() { WebView.platform = MyWebViewPlatform(); }); tearDownAll(() { @@ -1017,16 +984,19 @@ void main() { expect(platform.lastRequestHeaders, headers); }); }); + testWidgets('Set UserAgent', (WidgetTester tester) async { await tester.pumpWidget(const WebView( initialUrl: 'https://youtube.com', javascriptMode: JavascriptMode.unrestricted, )); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView!; + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single; - expect(platformWebView.userAgent, isNull); + expect(params.webSettings!.userAgent.value, isNull); await tester.pumpWidget(const WebView( initialUrl: 'https://youtube.com', @@ -1034,264 +1004,74 @@ void main() { userAgent: 'UA', )); - expect(platformWebView.userAgent, 'UA'); + final WebSettings settings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last; + expect(settings.userAgent.value, 'UA'); }); } -class FakePlatformWebView { - FakePlatformWebView(int? id, Map params) { - if (params.containsKey('initialUrl')) { - final String? initialUrl = params['initialUrl']; - if (initialUrl != null) { - history.add(initialUrl); - currentPosition++; - } - } - if (params.containsKey('javascriptChannelNames')) { - javascriptChannelNames = - List.from(params['javascriptChannelNames']); - } - javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; - hasNavigationDelegate = - params['settings']['hasNavigationDelegate'] ?? false; - debuggingEnabled = params['settings']['debuggingEnabled']; - userAgent = params['settings']['userAgent']; - zoomEnabled = params['settings']['zoomEnabled'] ?? true; - channel = MethodChannel( - 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); - channel.setMockMethodCallHandler(onMethodCall); - } - - late MethodChannel channel; - - List history = []; - int currentPosition = -1; - int amountOfReloadsOnCurrentUrl = 0; - bool hasCache = true; - - String? get currentUrl => history.isEmpty ? null : history[currentPosition]; - JavascriptMode? javascriptMode; - List? javascriptChannelNames; - - bool? hasNavigationDelegate; - bool? debuggingEnabled; - String? userAgent; - bool? zoomEnabled; - - String? lastRunJavaScriptString; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'loadUrl': - final Map request = call.arguments; - _loadUrl(request['url']); - return Future.sync(() {}); - case 'updateSettings': - if (call.arguments['jsMode'] != null) { - javascriptMode = JavascriptMode.values[call.arguments['jsMode']]; - } - if (call.arguments['hasNavigationDelegate'] != null) { - hasNavigationDelegate = call.arguments['hasNavigationDelegate']; - } - if (call.arguments['debuggingEnabled'] != null) { - debuggingEnabled = call.arguments['debuggingEnabled']; - } - userAgent = call.arguments['userAgent']; - if (call.arguments['zoomEnabled'] != null) { - zoomEnabled = call.arguments['zoomEnabled']; - } - break; - case 'canGoBack': - return Future.sync(() => currentPosition > 0); - case 'canGoForward': - return Future.sync(() => currentPosition < history.length - 1); - case 'goBack': - currentPosition = max(-1, currentPosition - 1); - return Future.sync(() {}); - case 'goForward': - currentPosition = min(history.length - 1, currentPosition + 1); - return Future.sync(() {}); - case 'reload': - amountOfReloadsOnCurrentUrl++; - return Future.sync(() {}); - case 'currentUrl': - return Future.value(currentUrl); - case 'runJavascriptReturningResult': - case 'evaluateJavascript': - lastRunJavaScriptString = call.arguments; - return Future.value(call.arguments); - case 'runJavascript': - lastRunJavaScriptString = call.arguments; - return Future.sync(() {}); - case 'addJavascriptChannels': - final List channelNames = List.from(call.arguments); - javascriptChannelNames!.addAll(channelNames); - break; - case 'removeJavascriptChannels': - final List channelNames = List.from(call.arguments); - javascriptChannelNames! - .removeWhere((String channel) => channelNames.contains(channel)); - break; - case 'clearCache': - hasCache = false; - return Future.sync(() {}); - } - return Future.sync(() {}); - } - - void fakeJavascriptPostMessage(String jsChannel, String message) { - final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { - 'channel': jsChannel, - 'message': message - }; - final ByteData data = codec - .encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) {}); - } - - // Fakes a main frame navigation that was initiated by the webview, e.g when - // the user clicks a link in the currently loaded page. - void fakeNavigate(String url) { - if (!hasNavigationDelegate!) { - print('no navigation delegate'); - _loadUrl(url); - return; - } - final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { - 'url': url, - 'isForMainFrame': true - }; - final ByteData data = - codec.encodeMethodCall(MethodCall('navigationRequest', arguments)); - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) { - final bool allow = codec.decodeEnvelope(data!); - if (allow) { - _loadUrl(url); - } - }); - } - - void fakeOnPageStartedCallback() { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onPageStarted', - {'url': currentUrl}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - data, - (ByteData? data) {}, - ); - } - - void fakeOnPageFinishedCallback() { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onPageFinished', - {'url': currentUrl}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - channel.name, - data, - (ByteData? data) {}, - ); - } - - void fakeOnProgressCallback(int progress) { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onProgress', - {'progress': progress}, - )); - - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData? data) {}); - } - - void _loadUrl(String? url) { - history = history.sublist(0, currentPosition + 1); - history.add(url); - currentPosition++; - amountOfReloadsOnCurrentUrl = 0; - } +List captureBuildArgs( + MockWebViewPlatform mockWebViewPlatform, { + bool context = false, + bool creationParams = false, + bool webViewPlatformCallbacksHandler = false, + bool javascriptChannelRegistry = false, + bool onWebViewPlatformCreated = false, + bool gestureRecognizers = false, +}) { + return verify(mockWebViewPlatform.build( + context: context ? captureAnyNamed('context') : anyNamed('context'), + creationParams: creationParams + ? captureAnyNamed('creationParams') + : anyNamed('creationParams'), + webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler + ? captureAnyNamed('webViewPlatformCallbacksHandler') + : anyNamed('webViewPlatformCallbacksHandler'), + javascriptChannelRegistry: javascriptChannelRegistry + ? captureAnyNamed('javascriptChannelRegistry') + : anyNamed('javascriptChannelRegistry'), + onWebViewPlatformCreated: onWebViewPlatformCreated + ? captureAnyNamed('onWebViewPlatformCreated') + : anyNamed('onWebViewPlatformCreated'), + gestureRecognizers: gestureRecognizers + ? captureAnyNamed('gestureRecognizers') + : anyNamed('gestureRecognizers'), + )).captured; } -class _FakePlatformViewsController { - FakePlatformWebView? lastCreatedView; - - Future fakePlatformViewsMethodHandler(MethodCall call) { - switch (call.method) { - case 'create': - final Map args = call.arguments; - final Map params = _decodeParams(args['params'])!; - lastCreatedView = FakePlatformWebView( - args['id'], - params, - ); - return Future.sync(() => 1); - default: - return Future.sync(() {}); - } - } +// This Widget ensures that onWebViewPlatformCreated is only called once when +// making multiple calls to `WidgetTester.pumpWidget` with different parameters +// for the WebView. +class TestPlatformWebView extends StatefulWidget { + const TestPlatformWebView({ + Key? key, + required this.mockWebViewPlatformController, + this.onWebViewPlatformCreated, + }) : super(key: key); - void reset() { - lastCreatedView = null; - } -} + final MockWebViewPlatformController mockWebViewPlatformController; + final WebViewPlatformCreatedCallback? onWebViewPlatformCreated; -Map? _decodeParams(Uint8List paramsMessage) { - final ByteBuffer buffer = paramsMessage.buffer; - final ByteData messageBytes = buffer.asByteData( - paramsMessage.offsetInBytes, - paramsMessage.lengthInBytes, - ); - return const StandardMessageCodec().decodeMessage(messageBytes); + @override + State createState() => TestPlatformWebViewState(); } -class _FakeCookieManager { - _FakeCookieManager() { - final MethodChannel channel = const MethodChannel( - 'plugins.flutter.io/cookie_manager', - StandardMethodCodec(), - ); - channel.setMockMethodCallHandler(onMethodCall); - } - - bool hasCookies = true; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'clearCookies': - bool hadCookies = false; - if (hasCookies) { - hadCookies = true; - hasCookies = false; - } - return Future.sync(() { - return hadCookies; - }); +class TestPlatformWebViewState extends State { + @override + void initState() { + super.initState(); + WebViewPlatformCreatedCallback? onWebViewPlatformCreated = + widget.onWebViewPlatformCreated; + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(widget.mockWebViewPlatformController); } - return Future.sync(() => true); } - void reset() { - hasCookies = true; + @override + Widget build(BuildContext context) { + return Container(); } } @@ -1381,10 +1161,3 @@ class MatchesCreationParams extends Matcher { .matches(creationParams.javascriptChannelNames, matchState); } } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart new file mode 100644 index 000000000000..b9d43cc6f182 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart @@ -0,0 +1,178 @@ +// Copyright 2013 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. + +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter/test/webview_flutter_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i9; + +import 'package:flutter/foundation.dart' as _i7; +import 'package:flutter/gestures.dart' as _i8; +import 'package:flutter/widgets.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart' + as _i6; +import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform.dart' + as _i3; +import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_callbacks_handler.dart' + as _i5; +import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_controller.dart' + as _i10; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i4; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeWidget_0 extends _i1.Fake implements _i2.Widget { + @override + String toString({_i2.DiagnosticLevel? minLevel = _i2.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [WebViewPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatform extends _i1.Mock implements _i3.WebViewPlatform { + MockWebViewPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Widget build( + {_i2.BuildContext? context, + _i4.CreationParams? creationParams, + _i5.WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, + _i6.JavascriptChannelRegistry? javascriptChannelRegistry, + _i3.WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set<_i7.Factory<_i8.OneSequenceGestureRecognizer>>? + gestureRecognizers}) => + (super.noSuchMethod( + Invocation.method(#build, [], { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers + }), + returnValue: _FakeWidget_0()) as _i2.Widget); + @override + _i9.Future clearCookies() => + (super.noSuchMethod(Invocation.method(#clearCookies, []), + returnValue: Future.value(false)) as _i9.Future); + @override + String toString() => super.toString(); +} + +/// A class which mocks [WebViewPlatformController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformController extends _i1.Mock + implements _i10.WebViewPlatformController { + MockWebViewPlatformController() { + _i1.throwOnMissingStub(this); + } + + @override + _i9.Future loadUrl(String? url, Map? headers) => + (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future updateSettings(_i4.WebSettings? setting) => + (super.noSuchMethod(Invocation.method(#updateSettings, [setting]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future currentUrl() => + (super.noSuchMethod(Invocation.method(#currentUrl, []), + returnValue: Future.value()) as _i9.Future); + @override + _i9.Future canGoBack() => + (super.noSuchMethod(Invocation.method(#canGoBack, []), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future canGoForward() => + (super.noSuchMethod(Invocation.method(#canGoForward, []), + returnValue: Future.value(false)) as _i9.Future); + @override + _i9.Future goBack() => + (super.noSuchMethod(Invocation.method(#goBack, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future goForward() => + (super.noSuchMethod(Invocation.method(#goForward, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future reload() => + (super.noSuchMethod(Invocation.method(#reload, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future clearCache() => + (super.noSuchMethod(Invocation.method(#clearCache, []), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future evaluateJavascript(String? javascript) => + (super.noSuchMethod(Invocation.method(#evaluateJavascript, [javascript]), + returnValue: Future.value('')) as _i9.Future); + @override + _i9.Future runJavascript(String? javascript) => + (super.noSuchMethod(Invocation.method(#runJavascript, [javascript]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future runJavascriptReturningResult(String? javascript) => + (super.noSuchMethod( + Invocation.method(#runJavascriptReturningResult, [javascript]), + returnValue: Future.value('')) as _i9.Future); + @override + _i9.Future addJavascriptChannels(Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method(#addJavascriptChannels, [javascriptChannelNames]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future removeJavascriptChannels( + Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method( + #removeJavascriptChannels, [javascriptChannelNames]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future getTitle() => + (super.noSuchMethod(Invocation.method(#getTitle, []), + returnValue: Future.value()) as _i9.Future); + @override + _i9.Future scrollTo(int? x, int? y) => + (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future scrollBy(int? x, int? y) => + (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i9.Future); + @override + _i9.Future getScrollX() => + (super.noSuchMethod(Invocation.method(#getScrollX, []), + returnValue: Future.value(0)) as _i9.Future); + @override + _i9.Future getScrollY() => + (super.noSuchMethod(Invocation.method(#getScrollY, []), + returnValue: Future.value(0)) as _i9.Future); + @override + String toString() => super.toString(); +}