diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index f599889c0d5e..b499cbab6f8d 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.2.0 + +* Added `runJavascript` and `runJavascriptForResult` to supersede `evaluateJavascript`. +* Deprecated `evaluateJavascript`. + ## 2.1.2 * Fix typos in the README. diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index 3379bafa2346..9279f31a1e3f 100644 --- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -70,6 +70,28 @@ void main() { expect(currentUrl, secondaryUrl); }, skip: _skipDueToIssue86757); + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + // ignore: deprecated_member_use + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = @@ -108,12 +130,12 @@ void main() { await pageLoads.stream.firstWhere((String url) => url == currentUrl); final String content = await controller - .evaluateJavascript('document.documentElement.innerText'); + .runJavascriptReturningResult('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); }, skip: Platform.isAndroid && _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. - testWidgets('JavaScriptChannel', (WidgetTester tester) async { + testWidgets('JavascriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageStarted = Completer(); @@ -153,11 +175,7 @@ void main() { await pageLoaded.future; expect(messagesReceived, isEmpty); - // Append a return value "1" in the end will prevent an iOS platform exception. - // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 - // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. - // https://github.com/flutter/flutter/issues/66318 - await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + await controller.runJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); }, skip: Platform.isAndroid && _skipDueToIssue86757); @@ -404,7 +422,8 @@ void main() { WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -433,7 +452,7 @@ void main() { controller = await controllerCompleter.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -464,7 +483,8 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageLoaded = Completer(); @@ -492,7 +512,7 @@ void main() { await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); @@ -542,7 +562,7 @@ void main() { await videoPlaying.future; String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); @@ -594,7 +614,7 @@ void main() { await videoPlaying.future; String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(true)); }, skip: Platform.isAndroid); }); @@ -660,7 +680,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -694,7 +715,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -730,7 +751,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageStarted = Completer(); @@ -763,7 +785,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); }); @@ -1028,15 +1050,16 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - final String viewportRectJSON = await _evaluateJavascript( + final String viewportRectJSON = await _runJavascriptReturningResult( controller, 'JSON.stringify(viewport.getBoundingClientRect())'); final Map viewportRectRelativeToViewport = jsonDecode(viewportRectJSON); // Check that the input is originally outside of the viewport. - final String initialInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final String initialInputClientRectJSON = + await _runJavascriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map initialInputClientRectRelativeToViewport = jsonDecode(initialInputClientRectJSON); @@ -1045,12 +1068,13 @@ void main() { viewportRectRelativeToViewport['bottom'], isFalse); - await controller.evaluateJavascript('inputEl.focus()'); + await controller.runJavascript('inputEl.focus()'); // Check that focusing the input brought it into view. - final String lastInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final String lastInputClientRectJSON = + await _runJavascriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map lastInputClientRectRelativeToViewport = jsonDecode(lastInputClientRectJSON); @@ -1106,7 +1130,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); @@ -1237,7 +1261,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.youtube.com/"'); + .runJavascript('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 @@ -1277,7 +1301,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); @@ -1332,7 +1356,7 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -1368,7 +1392,7 @@ void main() { await pageLoaded.future; pageLoaded = Completer(); - await controller.evaluateJavascript('window.open("$secondaryUrl")'); + await controller.runJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); expect(controller.currentUrl(), completion(secondaryUrl)); @@ -1382,7 +1406,7 @@ void main() { ); testWidgets( - 'javascript does not run in parent window', + 'JavaScript does not run in parent window', (WidgetTester tester) async { final String iframe = ''' @@ -1439,9 +1463,10 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoadCompleter.future; - expect(controller.evaluateJavascript('iframeLoaded'), completion('true')); + expect(controller.runJavascriptReturningResult('iframeLoaded'), + completion('true')); expect( - controller.evaluateJavascript( + controller.runJavascriptReturningResult( 'document.querySelector("p") && document.querySelector("p").textContent'), completion('null'), ); @@ -1461,13 +1486,13 @@ String _webviewBool(bool value) { /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. Future _getUserAgent(WebViewController controller) async { - return _evaluateJavascript(controller, 'navigator.userAgent;'); + return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); } -Future _evaluateJavascript( +Future _runJavascriptReturningResult( WebViewController controller, String js) async { if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.evaluateJavascript(js); + return await controller.runJavascriptReturningResult(js); } - return jsonDecode(await controller.evaluateJavascript(js)); + return jsonDecode(await controller.runJavascriptReturningResult(js)); } diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index c456a9691455..2fd2087378ba 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -210,14 +210,14 @@ class SampleMenu extends StatelessWidget { WebViewController controller, BuildContext context) async { // Send a message with the user agent string to the Toaster JavaScript channel we registered // with the WebView. - await controller.evaluateJavascript( + await controller.runJavascript( 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); } void _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = - await controller.evaluateJavascript('document.cookie'); + await controller.runJavascriptReturningResult('document.cookie'); // ignore: deprecated_member_use Scaffold.of(context).showSnackBar(SnackBar( content: Column( @@ -232,7 +232,7 @@ class SampleMenu extends StatelessWidget { } void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( + await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); // ignore: deprecated_member_use Scaffold.of(context).showSnackBar(const SnackBar( @@ -241,7 +241,7 @@ class SampleMenu extends StatelessWidget { } void _onListCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript('caches.keys()' + await controller.runJavascript('caches.keys()' '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Toaster.postMessage(caches))'); } diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart index 7699cc46c5d3..410a99599d4f 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -148,7 +148,7 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; - /// Whether Javascript execution is enabled. + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. @@ -221,9 +221,9 @@ class WebView extends StatefulWidget { /// When [onPageFinished] is invoked on Android, the page being rendered may /// not be updated yet. /// - /// When invoked on iOS or Android, any Javascript code that is embedded + /// When invoked on iOS or Android, any JavaScript code that is embedded /// directly in the HTML has been loaded and code injected with - /// [WebViewController.evaluateJavascript] can assume this. + /// [WebViewController.runJavascript] or [WebViewController.runJavascriptReturningResult] can assume this. final PageFinishedCallback? onPageFinished; /// Invoked when a page is loading. @@ -594,27 +594,75 @@ class WebViewController { /// /// On iOS depending on the value type the return value would be one of: /// - /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). - /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// - For primitive JavaScript types: the value string formatted + /// (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray + /// (e.g '(1,2,3), note that the string for NSArray is formatted and might + /// contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete + /// the Future with an error. /// - /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the - /// evaluated expression is not supported as described above. + /// The Future completes with an error if a JavaScript error occurred, + /// or on iOS, if the type of the evaluated expression is + /// not supported as described above. /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// When evaluating JavaScript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript /// embedded in the main frame HTML has been loaded. + @Deprecated('Use [runJavascript] or [runJavascriptReturningResult]') Future evaluateJavascript(String javascriptString) { if (_settings.javascriptMode == JavascriptMode.disabled) { return Future.error(FlutterError( 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method return _webViewPlatformController.evaluateJavascript(javascriptString); } + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. + /// + /// When running JavaScript in a [WebView], it is best practice to wait for + // the [WebView.onPageFinished] callback. This guarantees all the JavaScript + // embedded in the main frame HTML has been loaded. + Future runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// Runs the given JavaScript in the context of the current page, + /// and returns the result. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted + /// (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray + /// (e.g '(1,2,3), note that the string for NSArray is formatted and might + /// contain newlines and extra spaces.'). + /// + /// The Future completes with an error if a JavaScript error occurred, + /// or if the type the given expression evaluates to is unsupported. + /// Unsupported values include certain non primitive types on iOS, as well as + /// `undefined` or `null` on iOS 14+. + /// + /// When evaluating JavaScript in a [WebView], it is best practice to wait + /// for the [WebView.onPageFinished] callback. This guarantees all the + /// JavaScript embedded in the main frame HTML has been loaded. + Future runJavascriptReturningResult(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); + } + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); + } + /// Returns the title of the currently loaded page. Future getTitle() { return _webViewPlatformController.getTitle(); diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index dabfe0d0d14a..95a86fa018ea 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.1.2 +version: 2.2.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,9 +19,9 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.0.0 - webview_flutter_android: ^2.0.13 - webview_flutter_wkwebview: ^2.0.13 + webview_flutter_platform_interface: ^1.2.0 + webview_flutter_android: ^2.2.0 + webview_flutter_wkwebview: ^2.2.0 dev_dependencies: flutter_driver: 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 f7d09266a64b..8d0f754a529f 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -372,7 +372,9 @@ void main() { ), ); expect( - await controller.evaluateJavascript("fake js string"), "fake js string", + // ignore: deprecated_member_use_from_same_package + await controller.evaluateJavascript("fake js string"), + "fake js string", reason: 'should get the argument'); }); @@ -389,11 +391,80 @@ void main() { ), ); expect( + // ignore: deprecated_member_use_from_same_package () => controller.evaluateJavascript('fake js string'), throwsA(anything), ); }); + testWidgets('runJavaScript', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + await controller.runJavascript('fake js string'); + expect(fakePlatformViewsController.lastCreatedView?.lastRunJavaScriptString, + 'fake js string'); + }); + + testWidgets('runJavaScript with JavascriptMode disabled', + (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.disabled, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + () => controller.runJavascript('fake js string'), + throwsA(anything), + ); + }); + + testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect(await controller.runJavascriptReturningResult("fake js string"), + "fake js string", + reason: 'should get the argument'); + }); + + testWidgets('runJavaScriptReturningResult with JavascriptMode disabled', + (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.disabled, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + () => controller.runJavascriptReturningResult('fake js string'), + throwsA(anything), + ); + }); + testWidgets('Cookies can be cleared once', (WidgetTester tester) async { await tester.pumpWidget( const WebView( @@ -960,6 +1031,8 @@ class FakePlatformWebView { bool? debuggingEnabled; String? userAgent; + String? lastRunJavaScriptString; + Future onMethodCall(MethodCall call) { switch (call.method) { case 'loadUrl': @@ -993,8 +1066,13 @@ class FakePlatformWebView { 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);