// Copyright 2019, the Chromium project authors. Please see the AUTHORS file // for details. 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:convert'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:e2e/e2e.dart'; void main() { E2EWidgetsFlutterBinding.ensureInitialized(); testWidgets('initalUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), initialUrl: 'https://flutter.dev/', onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, ), ), ); final WebViewController controller = await controllerCompleter.future; final String currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://flutter.dev/'); }); testWidgets('loadUrl', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), initialUrl: 'https://flutter.dev/', onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, ), ), ); final WebViewController controller = await controllerCompleter.future; await controller.loadUrl('https://www.google.com/'); final String currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://www.google.com/'); }); // enable this once https://github.com/flutter/flutter/issues/31510 // is resolved. testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final StreamController pageStarts = StreamController(); final StreamController pageLoads = StreamController(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), initialUrl: 'https://flutter.dev/', onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, onPageStarted: (String url) { pageStarts.add(url); }, onPageFinished: (String url) { pageLoads.add(url); }, ), ), ); final WebViewController controller = await controllerCompleter.future; final Map headers = { 'test_header': 'flutter_test_header' }; await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', headers: headers); final String currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); await pageStarts.stream.firstWhere((String url) => url == currentUrl); await pageLoads.stream.firstWhere((String url) => url == currentUrl); final String content = await controller .evaluateJavascript('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); }); testWidgets('JavaScriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); final List messagesReceived = []; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), // This is the data URL for: '' initialUrl: 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, // TODO(iskakaushik): Remove this when collection literals makes it to stable. // ignore: prefer_collection_literals javascriptChannels: [ JavascriptChannel( name: 'Echo', onMessageReceived: (JavascriptMessage message) { messagesReceived.add(message.message); }, ), ].toSet(), onPageStarted: (String url) { pageStarted.complete(null); }, onPageFinished: (String url) { pageLoaded.complete(null); }, ), ), ); final WebViewController controller = await controllerCompleter.future; await pageStarted.future; await pageLoaded.future; expect(messagesReceived, isEmpty); await controller.evaluateJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); }); testWidgets('resize webview', (WidgetTester tester) async { final String resizeTest = ''' Resize test '''; final String resizeTestBase64 = base64Encode(const Utf8Encoder().convert(resizeTest)); final Completer resizeCompleter = Completer(); final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); final Completer controllerCompleter = Completer(); final GlobalKey key = GlobalKey(); final WebView webView = WebView( key: key, initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, // TODO(iskakaushik): Remove this when collection literals makes it to stable. // ignore: prefer_collection_literals javascriptChannels: [ JavascriptChannel( name: 'Resize', onMessageReceived: (JavascriptMessage message) { resizeCompleter.complete(true); }, ), ].toSet(), onPageStarted: (String url) { pageStarted.complete(null); }, onPageFinished: (String url) { pageLoaded.complete(null); }, javascriptMode: JavascriptMode.unrestricted, ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ SizedBox( width: 200, height: 200, child: webView, ), ], ), ), ); await controllerCompleter.future; await pageStarted.future; await pageLoaded.future; expect(resizeCompleter.isCompleted, false); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Column( children: [ SizedBox( width: 400, height: 400, child: webView, ), ], ), ), ); await resizeCompleter.future; }); testWidgets('set custom userAgent', (WidgetTester tester) async { final Completer controllerCompleter1 = Completer(); final GlobalKey _globalKey = GlobalKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: _globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent1', onWebViewCreated: (WebViewController controller) { controllerCompleter1.complete(controller); }, ), ), ); final WebViewController controller1 = await controllerCompleter1.future; final String customUserAgent1 = await _getUserAgent(controller1); expect(customUserAgent1, 'Custom_User_Agent1'); // rebuild the WebView with a different user agent. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: _globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent2', ), ), ); final String customUserAgent2 = await _getUserAgent(controller1); expect(customUserAgent2, 'Custom_User_Agent2'); }); testWidgets('use default platform userAgent after webView is rebuilt', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final GlobalKey _globalKey = GlobalKey(); // Build the webView with no user agent to get the default platform user agent. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: _globalKey, initialUrl: 'https://flutter.dev/', javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, ), ), ); final WebViewController controller = await controllerCompleter.future; final String defaultPlatformUserAgent = await _getUserAgent(controller); // rebuild the WebView with a custom user agent. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: _globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, userAgent: 'Custom_User_Agent', ), ), ); final String customUserAgent = await _getUserAgent(controller); expect(customUserAgent, 'Custom_User_Agent'); // rebuilds the WebView with no user agent. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: _globalKey, initialUrl: 'about:blank', javascriptMode: JavascriptMode.unrestricted, ), ), ); final String customUserAgent2 = await _getUserAgent(controller); expect(customUserAgent2, defaultPlatformUserAgent); }); group('Media playback policy', () { String audioTestBase64; setUpAll(() async { final ByteData audioData = await rootBundle.load('assets/sample_audio.ogg'); final String base64AudioData = base64Encode(Uint8List.view(audioData.buffer)); final String audioTest = ''' Audio auto play '''; audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); }); testWidgets('Auto media playback', (WidgetTester tester) async { Completer controllerCompleter = Completer(); Completer pageStarted = Completer(); Completer pageLoaded = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, onPageStarted: (String url) { pageStarted.complete(null); }, onPageFinished: (String url) { pageLoaded.complete(null); }, initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, ), ), ); WebViewController controller = await controllerCompleter.future; await pageStarted.future; await pageLoaded.future; String isPaused = await controller.evaluateJavascript('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); pageStarted = Completer(); pageLoaded = Completer(); // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, onPageStarted: (String url) { pageStarted.complete(null); }, onPageFinished: (String url) { pageLoaded.complete(null); }, initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, ), ), ); controller = await controllerCompleter.future; await pageStarted.future; await pageLoaded.future; isPaused = await controller.evaluateJavascript('isPaused();'); expect(isPaused, _webviewBool(true)); }); testWidgets('Changes to initialMediaPlaybackPolocy are ignored', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); Completer pageStarted = Completer(); Completer pageLoaded = Completer(); final GlobalKey key = GlobalKey(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: key, initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, onPageStarted: (String url) { pageStarted.complete(null); }, onPageFinished: (String url) { pageLoaded.complete(null); }, initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, ), ), ); final WebViewController controller = await controllerCompleter.future; await pageStarted.future; await pageLoaded.future; String isPaused = await controller.evaluateJavascript('isPaused();'); expect(isPaused, _webviewBool(false)); pageStarted = Completer(); pageLoaded = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: key, initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, onPageStarted: (String url) { pageStarted.complete(null); }, onPageFinished: (String url) { pageLoaded.complete(null); }, initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, ), ), ); await controller.reload(); await pageStarted.future; await pageLoaded.future; isPaused = await controller.evaluateJavascript('isPaused();'); expect(isPaused, _webviewBool(false)); }); }); testWidgets('getTitle', (WidgetTester tester) async { final String getTitleTest = ''' Some title '''; final String getTitleTestBase64 = base64Encode(const Utf8Encoder().convert(getTitleTest)); final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); final Completer controllerCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, onPageStarted: (String url) { pageStarted.complete(null); }, onPageFinished: (String url) { pageLoaded.complete(null); }, ), ), ); final WebViewController controller = await controllerCompleter.future; await pageStarted.future; await pageLoaded.future; final String title = await controller.getTitle(); expect(title, 'Some title'); }); group('NavigationDelegate', () { final String blankPage = ""; final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + base64Encode(const Utf8Encoder().convert(blankPage)); testWidgets('can allow requests', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final StreamController pageLoads = StreamController.broadcast(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), initialUrl: blankPageEncoded, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, navigationDelegate: (NavigationRequest request) { return (request.url.contains('youtube.com')) ? NavigationDecision.prevent : NavigationDecision.navigate; }, onPageFinished: (String url) => pageLoads.add(url), ), ), ); await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller .evaluateJavascript('location.href = "https://www.google.com/"'); await pageLoads.stream.first; // Wait for the next page load. final String currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://www.google.com/'); }); testWidgets('can block requests', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final StreamController pageLoads = StreamController.broadcast(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), initialUrl: blankPageEncoded, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, navigationDelegate: (NavigationRequest request) { return (request.url.contains('youtube.com')) ? NavigationDecision.prevent : NavigationDecision.navigate; }, onPageFinished: (String url) => pageLoads.add(url), ), ), ); await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller .evaluateJavascript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order // to give the test a chance to fail. await pageLoads.stream.first .timeout(const Duration(milliseconds: 500), onTimeout: () => null); final String currentUrl = await controller.currentUrl(); expect(currentUrl, isNot(contains('youtube.com'))); }); testWidgets('supports asynchronous decisions', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final StreamController pageLoads = StreamController.broadcast(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), initialUrl: blankPageEncoded, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, navigationDelegate: (NavigationRequest request) async { NavigationDecision decision = NavigationDecision.prevent; decision = await Future.delayed( const Duration(milliseconds: 10), () => NavigationDecision.navigate); return decision; }, onPageFinished: (String url) => pageLoads.add(url), ), ), ); await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller .evaluateJavascript('location.href = "https://www.google.com"'); await pageLoads.stream.first; // Wait for second page to load. final String currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://www.google.com/'); }); }); testWidgets('launches with gestureNavigationEnabled on iOS', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SizedBox( width: 400, height: 300, child: WebView( key: GlobalKey(), initialUrl: 'https://flutter.dev/', gestureNavigationEnabled: true, onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, ), ), ), ); final WebViewController controller = await controllerCompleter.future; final String currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://flutter.dev/'); }); testWidgets('target _blank opens in same window', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageLoaded = Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: WebView( key: GlobalKey(), onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, onPageFinished: (String url) { pageLoaded.complete(null); }, ), ), ); final WebViewController controller = await controllerCompleter.future; await controller.evaluateJavascript('window.open("about:blank", "_blank")'); await pageLoaded.future; final String currentUrl = await controller.currentUrl(); expect(currentUrl, 'about:blank'); }); } // JavaScript booleans evaluate to different string values on Android and iOS. // This utility method returns the string boolean value of the current platform. String _webviewBool(bool value) { if (defaultTargetPlatform == TargetPlatform.iOS) { return value ? '1' : '0'; } return value ? 'true' : 'false'; } /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. Future _getUserAgent(WebViewController controller) async { if (defaultTargetPlatform == TargetPlatform.iOS) { return await controller.evaluateJavascript('navigator.userAgent;'); } return jsonDecode( await controller.evaluateJavascript('navigator.userAgent;')); }