[webview_flutter] Deprecate evaluateJavascript in favour of runJavaScript and runJavaScriptForResult. (#4403)
diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md
index f599889..b499cba 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 3379baf..9279f31 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 @@
expect(currentUrl, secondaryUrl);
}, skip: _skipDueToIssue86757);
+ testWidgets('evaluateJavascript', (WidgetTester tester) async {
+ final Completer<WebViewController> controllerCompleter =
+ Completer<WebViewController>();
+ 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<WebViewController> controllerCompleter =
@@ -108,12 +130,12 @@
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<WebViewController> controllerCompleter =
Completer<WebViewController>();
final Completer<void> pageStarted = Completer<void>();
@@ -153,11 +175,7 @@
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(<String>['hello']));
}, skip: Platform.isAndroid && _skipDueToIssue86757);
@@ -404,7 +422,8 @@
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<WebViewController>();
@@ -433,7 +452,7 @@
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 @@
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<void>();
@@ -492,7 +512,7 @@
await pageLoaded.future;
- isPaused = await controller.evaluateJavascript('isPaused();');
+ isPaused = await controller.runJavascriptReturningResult('isPaused();');
expect(isPaused, _webviewBool(false));
});
@@ -542,7 +562,7 @@
await videoPlaying.future;
String fullScreen =
- await controller.evaluateJavascript('isFullScreen();');
+ await controller.runJavascriptReturningResult('isFullScreen();');
expect(fullScreen, _webviewBool(false));
});
@@ -594,7 +614,7 @@
await videoPlaying.future;
String fullScreen =
- await controller.evaluateJavascript('isFullScreen();');
+ await controller.runJavascriptReturningResult('isFullScreen();');
expect(fullScreen, _webviewBool(true));
}, skip: Platform.isAndroid);
});
@@ -660,7 +680,8 @@
await pageStarted.future;
await pageLoaded.future;
- String isPaused = await controller.evaluateJavascript('isPaused();');
+ String isPaused =
+ await controller.runJavascriptReturningResult('isPaused();');
expect(isPaused, _webviewBool(false));
controllerCompleter = Completer<WebViewController>();
@@ -694,7 +715,7 @@
await pageStarted.future;
await pageLoaded.future;
- isPaused = await controller.evaluateJavascript('isPaused();');
+ isPaused = await controller.runJavascriptReturningResult('isPaused();');
expect(isPaused, _webviewBool(true));
});
@@ -730,7 +751,8 @@
await pageStarted.future;
await pageLoaded.future;
- String isPaused = await controller.evaluateJavascript('isPaused();');
+ String isPaused =
+ await controller.runJavascriptReturningResult('isPaused();');
expect(isPaused, _webviewBool(false));
pageStarted = Completer<void>();
@@ -763,7 +785,7 @@
await pageStarted.future;
await pageLoaded.future;
- isPaused = await controller.evaluateJavascript('isPaused();');
+ isPaused = await controller.runJavascriptReturningResult('isPaused();');
expect(isPaused, _webviewBool(false));
});
});
@@ -1028,15 +1050,16 @@
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<String, dynamic> 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<String, dynamic> initialInputClientRectRelativeToViewport =
jsonDecode(initialInputClientRectJSON);
@@ -1045,12 +1068,13 @@
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<String, dynamic> lastInputClientRectRelativeToViewport =
jsonDecode(lastInputClientRectJSON);
@@ -1106,7 +1130,7 @@
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 @@
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 @@
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 @@
),
);
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 @@
await pageLoaded.future;
pageLoaded = Completer<void>();
- await controller.evaluateJavascript('window.open("$secondaryUrl")');
+ await controller.runJavascript('window.open("$secondaryUrl")');
await pageLoaded.future;
pageLoaded = Completer<void>();
expect(controller.currentUrl(), completion(secondaryUrl));
@@ -1382,7 +1406,7 @@
);
testWidgets(
- 'javascript does not run in parent window',
+ 'JavaScript does not run in parent window',
(WidgetTester tester) async {
final String iframe = '''
<!DOCTYPE html>
@@ -1439,9 +1463,10 @@
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 @@
/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests.
Future<String> _getUserAgent(WebViewController controller) async {
- return _evaluateJavascript(controller, 'navigator.userAgent;');
+ return _runJavascriptReturningResult(controller, 'navigator.userAgent;');
}
-Future<String> _evaluateJavascript(
+Future<String> _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 c456a96..2fd2087 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 @@
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 @@
}
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 @@
}
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 7699cc4..410a995 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 @@
/// 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 @@
/// 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 @@
///
/// 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<String> evaluateJavascript(String javascriptString) {
if (_settings.javascriptMode == JavascriptMode.disabled) {
return Future<String>.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<void> runJavascript(String javaScriptString) {
+ if (_settings.javascriptMode == JavascriptMode.disabled) {
+ return Future<void>.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<String> runJavascriptReturningResult(String javaScriptString) {
+ if (_settings.javascriptMode == JavascriptMode.disabled) {
+ return Future<String>.error(FlutterError(
+ 'JavaScript mode must be enabled/unrestricted when calling runJavascriptReturningResult.'));
+ }
+ return _webViewPlatformController
+ .runJavascriptReturningResult(javaScriptString);
+ }
+
/// Returns the title of the currently loaded page.
Future<String?> getTitle() {
return _webViewPlatformController.getTitle();
diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml
index dabfe0d..95a86fa 100644
--- a/packages/webview_flutter/webview_flutter/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter/pubspec.yaml
@@ -2,7 +2,7 @@
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 @@
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 f7d0926..8d0f754 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 @@
),
);
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 @@
),
);
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 @@
bool? debuggingEnabled;
String? userAgent;
+ String? lastRunJavaScriptString;
+
Future<dynamic> onMethodCall(MethodCall call) {
switch (call.method) {
case 'loadUrl':
@@ -993,8 +1066,13 @@
return Future<void>.sync(() {});
case 'currentUrl':
return Future<String?>.value(currentUrl);
+ case 'runJavascriptReturningResult':
case 'evaluateJavascript':
+ lastRunJavaScriptString = call.arguments;
return Future<dynamic>.value(call.arguments);
+ case 'runJavascript':
+ lastRunJavaScriptString = call.arguments;
+ return Future<void>.sync(() {});
case 'addJavascriptChannels':
final List<String> channelNames = List<String>.from(call.arguments);
javascriptChannelNames!.addAll(channelNames);