From eac45de20d9ed25af3648fbbdb315b00a1e4d8b7 Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 29 Sep 2023 21:39:07 +0200 Subject: [PATCH] [webview_flutter_android] Adds Android implementation to override console log (#4702) Adds the Android implementation for registering a JavaScript console callback. This will allow developers to receive JavaScript console messages in a Dart callback. This PR contains the `webview_flutter_android` specific changes from PR #4541. Related issue: flutter/flutter#32908 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* --- .../webview_flutter_android/CHANGELOG.md | 4 + .../GeneratedAndroidWebView.java | 260 +++++++++++++++++- .../WebChromeClientFlutterApiImpl.java | 38 +++ .../WebChromeClientHostApiImpl.java | 21 ++ .../webviewflutter/WebChromeClientTest.java | 14 + .../webview_flutter_test.dart | 41 +++ .../example/lib/main.dart | 52 ++++ .../example/pubspec.yaml | 2 +- .../lib/src/android_proxy.dart | 3 + .../lib/src/android_webview.dart | 37 ++- .../lib/src/android_webview.g.dart | 150 +++++++++- .../lib/src/android_webview_api_impls.dart | 21 +- .../lib/src/android_webview_controller.dart | 51 ++++ .../pigeons/android_webview.dart | 54 ++++ .../webview_flutter_android/pubspec.yaml | 4 +- .../android_navigation_delegate_test.dart | 1 + .../test/android_webview_controller_test.dart | 110 ++++++++ ...android_webview_controller_test.mocks.dart | 50 +++- ...oid_webview_cookie_manager_test.mocks.dart | 23 +- .../test/android_webview_test.dart | 75 +++++ .../test/android_webview_test.mocks.dart | 25 ++ .../webview_android_widget_test.mocks.dart | 10 + .../test/test_android_webview.g.dart | 29 ++ 23 files changed, 1064 insertions(+), 11 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 4608d1caa44f..4fb48de10e32 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.11.0 + +* Adds support to register a callback to receive JavaScript console messages. See `AndroidWebViewController.onConsoleMessage`. + ## 3.10.1 * Bumps androidx.annotation:annotation from 1.5.0 to 1.7.0. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index 567e201b859c..d4cfc698f030 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -94,6 +94,62 @@ private FileChooserMode(final int index) { } } + /** + * Indicates the type of message logged to the console. + * + *

See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel. + */ + public enum ConsoleMessageLevel { + /** + * Indicates a message is logged for debugging. + * + *

See + * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#DEBUG. + */ + DEBUG(0), + /** + * Indicates a message is provided as an error. + * + *

See + * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#ERROR. + */ + ERROR(1), + /** + * Indicates a message is provided as a basic log message. + * + *

See + * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#LOG. + */ + LOG(2), + /** + * Indicates a message is provided as a tip. + * + *

See + * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#TIP. + */ + TIP(3), + /** + * Indicates a message is provided as a warning. + * + *

See + * https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#WARNING. + */ + WARNING(4), + /** + * Indicates a message with an unknown level. + * + *

This does not represent an actual value provided by the platform and only indicates a + * value was provided that isn't currently supported. + */ + UNKNOWN(5); + + final int index; + + private ConsoleMessageLevel(final int index) { + this.index = index; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static final class WebResourceRequestData { private @NonNull String url; @@ -409,6 +465,136 @@ ArrayList toList() { } } + /** + * Represents a JavaScript console message from WebCore. + * + *

See https://developer.android.com/reference/android/webkit/ConsoleMessage + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class ConsoleMessage { + private @NonNull Long lineNumber; + + public @NonNull Long getLineNumber() { + return lineNumber; + } + + public void setLineNumber(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"lineNumber\" is null."); + } + this.lineNumber = setterArg; + } + + private @NonNull String message; + + public @NonNull String getMessage() { + return message; + } + + public void setMessage(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"message\" is null."); + } + this.message = setterArg; + } + + private @NonNull ConsoleMessageLevel level; + + public @NonNull ConsoleMessageLevel getLevel() { + return level; + } + + public void setLevel(@NonNull ConsoleMessageLevel setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"level\" is null."); + } + this.level = setterArg; + } + + private @NonNull String sourceId; + + public @NonNull String getSourceId() { + return sourceId; + } + + public void setSourceId(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"sourceId\" is null."); + } + this.sourceId = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + ConsoleMessage() {} + + public static final class Builder { + + private @Nullable Long lineNumber; + + public @NonNull Builder setLineNumber(@NonNull Long setterArg) { + this.lineNumber = setterArg; + return this; + } + + private @Nullable String message; + + public @NonNull Builder setMessage(@NonNull String setterArg) { + this.message = setterArg; + return this; + } + + private @Nullable ConsoleMessageLevel level; + + public @NonNull Builder setLevel(@NonNull ConsoleMessageLevel setterArg) { + this.level = setterArg; + return this; + } + + private @Nullable String sourceId; + + public @NonNull Builder setSourceId(@NonNull String setterArg) { + this.sourceId = setterArg; + return this; + } + + public @NonNull ConsoleMessage build() { + ConsoleMessage pigeonReturn = new ConsoleMessage(); + pigeonReturn.setLineNumber(lineNumber); + pigeonReturn.setMessage(message); + pigeonReturn.setLevel(level); + pigeonReturn.setSourceId(sourceId); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(4); + toListResult.add(lineNumber); + toListResult.add(message); + toListResult.add(level == null ? null : level.index); + toListResult.add(sourceId); + return toListResult; + } + + static @NonNull ConsoleMessage fromList(@NonNull ArrayList list) { + ConsoleMessage pigeonResult = new ConsoleMessage(); + Object lineNumber = list.get(0); + pigeonResult.setLineNumber( + (lineNumber == null) + ? null + : ((lineNumber instanceof Integer) ? (Integer) lineNumber : (Long) lineNumber)); + Object message = list.get(1); + pigeonResult.setMessage((String) message); + Object level = list.get(2); + pigeonResult.setLevel(ConsoleMessageLevel.values()[(int) level]); + Object sourceId = list.get(3); + pigeonResult.setSourceId((String) sourceId); + return pigeonResult; + } + } + public interface Result { @SuppressWarnings("UnknownNullness") void success(T result); @@ -2401,6 +2587,9 @@ public interface WebChromeClientHostApi { void setSynchronousReturnValueForOnShowFileChooser( @NonNull Long instanceId, @NonNull Boolean value); + void setSynchronousReturnValueForOnConsoleMessage( + @NonNull Long instanceId, @NonNull Boolean value); + /** The codec used by WebChromeClientHostApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -2463,6 +2652,33 @@ static void setup( channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + Boolean valueArg = (Boolean) args.get(1); + try { + api.setSynchronousReturnValueForOnConsoleMessage( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), valueArg); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ @@ -2536,6 +2752,34 @@ static void setup( } } } + + private static class WebChromeClientFlutterApiCodec extends StandardMessageCodec { + public static final WebChromeClientFlutterApiCodec INSTANCE = + new WebChromeClientFlutterApiCodec(); + + private WebChromeClientFlutterApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return ConsoleMessage.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof ConsoleMessage) { + stream.write(128); + writeValue(stream, ((ConsoleMessage) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class WebChromeClientFlutterApi { private final @NonNull BinaryMessenger binaryMessenger; @@ -2551,7 +2795,7 @@ public interface Reply { } /** The codec used by WebChromeClientFlutterApi. */ static @NonNull MessageCodec getCodec() { - return new StandardMessageCodec(); + return WebChromeClientFlutterApiCodec.INSTANCE; } public void onProgressChanged( @@ -2656,6 +2900,20 @@ public void onGeolocationPermissionsHidePrompt( new ArrayList(Collections.singletonList(identifierArg)), channelReply -> callback.reply(null)); } + /** Callback to Dart function `WebChromeClient.onConsoleMessage`. */ + public void onConsoleMessage( + @NonNull Long instanceIdArg, + @NonNull ConsoleMessage messageArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, messageArg)), + channelReply -> callback.reply(null)); + } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebStorageHostApi { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java index f5097b6393cf..b383dfdba11e 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java @@ -6,6 +6,7 @@ import android.os.Build; import android.view.View; +import android.webkit.ConsoleMessage; import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.WebChromeClient; @@ -27,6 +28,24 @@ public class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { private final InstanceManager instanceManager; private final WebViewFlutterApiImpl webViewFlutterApi; + private static GeneratedAndroidWebView.ConsoleMessageLevel toConsoleMessageLevel( + ConsoleMessage.MessageLevel level) { + switch (level) { + case TIP: + return GeneratedAndroidWebView.ConsoleMessageLevel.TIP; + case LOG: + return GeneratedAndroidWebView.ConsoleMessageLevel.LOG; + case WARNING: + return GeneratedAndroidWebView.ConsoleMessageLevel.WARNING; + case ERROR: + return GeneratedAndroidWebView.ConsoleMessageLevel.ERROR; + case DEBUG: + return GeneratedAndroidWebView.ConsoleMessageLevel.DEBUG; + } + + return GeneratedAndroidWebView.ConsoleMessageLevel.UNKNOWN; + } + /** * Creates a Flutter api that sends messages to Dart. * @@ -149,6 +168,25 @@ public void onHideCustomView( callback); } + /** + * Sends a message to Dart to call `WebChromeClient.onConsoleMessage` on the Dart object + * representing `instance`. + */ + public void onConsoleMessage( + @NonNull WebChromeClient instance, + @NonNull ConsoleMessage message, + @NonNull Reply callback) { + super.onConsoleMessage( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(instance)), + new GeneratedAndroidWebView.ConsoleMessage.Builder() + .setLineNumber((long) message.lineNumber()) + .setMessage(message.message()) + .setLevel(toConsoleMessageLevel(message.messageLevel())) + .setSourceId(message.sourceId()) + .build(), + callback); + } + private long getIdentifierForClient(WebChromeClient webChromeClient) { final Long identifier = instanceManager.getIdentifierForStrongReference(webChromeClient); if (identifier == null) { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java index 635c6c30ee93..cb382d51f2b1 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -9,6 +9,7 @@ import android.os.Build; import android.os.Message; import android.view.View; +import android.webkit.ConsoleMessage; import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.ValueCallback; @@ -40,6 +41,7 @@ public class WebChromeClientHostApiImpl implements WebChromeClientHostApi { public static class WebChromeClientImpl extends SecureWebChromeClient { private final WebChromeClientFlutterApiImpl flutterApi; private boolean returnValueForOnShowFileChooser = false; + private boolean returnValueForOnConsoleMessage = false; /** * Creates a {@link WebChromeClient} that passes arguments of callbacks methods to Dart. @@ -107,10 +109,21 @@ public void onPermissionRequest(@NonNull PermissionRequest request) { flutterApi.onPermissionRequest(this, request, reply -> {}); } + @Override + public boolean onConsoleMessage(ConsoleMessage consoleMessage) { + flutterApi.onConsoleMessage(this, consoleMessage, reply -> {}); + return returnValueForOnConsoleMessage; + } + /** Sets return value for {@link #onShowFileChooser}. */ public void setReturnValueForOnShowFileChooser(boolean value) { returnValueForOnShowFileChooser = value; } + + /** Sets return value for {@link #onConsoleMessage}. */ + public void setReturnValueForOnConsoleMessage(boolean value) { + returnValueForOnConsoleMessage = value; + } } /** @@ -246,4 +259,12 @@ public void setSynchronousReturnValueForOnShowFileChooser( Objects.requireNonNull(instanceManager.getInstance(instanceId)); webChromeClient.setReturnValueForOnShowFileChooser(value); } + + @Override + public void setSynchronousReturnValueForOnConsoleMessage( + @NonNull Long instanceId, @NonNull Boolean value) { + final WebChromeClientImpl webChromeClient = + Objects.requireNonNull(instanceManager.getInstance(instanceId)); + webChromeClient.setReturnValueForOnConsoleMessage(value); + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java index 4e09b2e4b452..ef49dded9b56 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -16,6 +16,7 @@ import android.net.Uri; import android.os.Message; import android.view.View; +import android.webkit.ConsoleMessage; import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.WebChromeClient; @@ -162,4 +163,17 @@ public void onGeolocationPermissionsHidePrompt() { webChromeClient.onGeolocationPermissionsHidePrompt(); verify(mockFlutterApi).onGeolocationPermissionsHidePrompt(eq(webChromeClient), any()); } + + @Test + public void onConsoleMessage() { + webChromeClient.onConsoleMessage( + new ConsoleMessage("message", "sourceId", 23, ConsoleMessage.MessageLevel.ERROR)); + verify(mockFlutterApi).onConsoleMessage(eq(webChromeClient), any(), any()); + } + + @Test + public void setReturnValueForOnConsoleMessage() { + webChromeClient.setReturnValueForOnConsoleMessage(true); + assertTrue(webChromeClient.onConsoleMessage(null)); + } } diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart index 0e805dd2aef4..017e6961e60f 100644 --- a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart @@ -1310,6 +1310,47 @@ Future main() async { ); }, ); + + group('Logging', () { + testWidgets('can receive console log messages', + (WidgetTester tester) async { + const String testPage = ''' + + + + WebResourceError test + + +

Test page

+ + + '''; + + final Completer debugMessageReceived = Completer(); + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ); + unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted)); + + await controller.setOnConsoleMessage((JavaScriptConsoleMessage message) { + debugMessageReceived + .complete('${message.level.name}:${message.message}'); + }); + + await controller.loadHtmlString(testPage); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await expectLater( + debugMessageReceived.future, completion('debug:Debug message')); + }); + }); } /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index 781a98f87f7b..92a8fe2bd770 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -73,6 +73,40 @@ const String kTransparentBackgroundPage = ''' '''; +const String kLogExamplePage = ''' + + + +Load file or HTML string example + + + +

Local demo page

+

+ This page is used to test the forwarding of console logs to Dart. +

+ + + +
+ + + + + +
+ + + +'''; + class WebViewExample extends StatefulWidget { const WebViewExample({super.key, this.cookieManager}); @@ -202,6 +236,7 @@ enum MenuOptions { transparentBackground, setCookie, videoExample, + logExample, } class SampleMenu extends StatelessWidget { @@ -265,6 +300,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.videoExample: _onVideoExample(context); break; + case MenuOptions.logExample: + _onLogExample(); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -321,6 +359,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.transparentBackground, child: Text('Transparent background example'), ), + const PopupMenuItem( + value: MenuOptions.logExample, + child: Text('Log example'), + ), const PopupMenuItem( value: MenuOptions.videoExample, child: Text('Video example'), @@ -497,6 +539,16 @@ class SampleMenu extends StatelessWidget { return indexFile.path; } + + Future _onLogExample() { + webViewController + .setOnConsoleMessage((JavaScriptConsoleMessage consoleMessage) { + debugPrint( + '== JS == ${consoleMessage.level.name}: ${consoleMessage.message}'); + }); + + return webViewController.loadHtmlString(kLogExamplePage); + } } class NavigationControls extends StatelessWidget { diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index 286b847a0f97..acba2006e84a 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - webview_flutter_platform_interface: ^2.4.0 + webview_flutter_platform_interface: ^2.6.0 dev_dependencies: espresso: ^0.2.0 diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart index 508a31bbe5fc..f3d00ebd88e3 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart @@ -44,6 +44,9 @@ class AndroidWebViewProxy { onGeolocationPermissionsShowPrompt, void Function(android_webview.WebChromeClient instance)? onGeolocationPermissionsHidePrompt, + void Function(android_webview.WebChromeClient instance, + android_webview.ConsoleMessage message)? + onConsoleMessage, void Function( android_webview.WebChromeClient instance, android_webview.View view, diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart index 5ed2fc0351ef..d63f2c75cb5f 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -12,7 +12,8 @@ import 'android_webview.g.dart'; import 'android_webview_api_impls.dart'; import 'instance_manager.dart'; -export 'android_webview_api_impls.dart' show FileChooserMode; +export 'android_webview_api_impls.dart' + show ConsoleMessage, ConsoleMessageLevel, FileChooserMode; /// Root of the Java class hierarchy. /// @@ -1047,6 +1048,7 @@ class WebChromeClient extends JavaObject { this.onGeolocationPermissionsHidePrompt, this.onShowCustomView, this.onHideCustomView, + this.onConsoleMessage, @visibleForTesting super.binaryMessenger, @visibleForTesting super.instanceManager, }) : super.detached() { @@ -1068,6 +1070,7 @@ class WebChromeClient extends JavaObject { this.onGeolocationPermissionsHidePrompt, this.onShowCustomView, this.onHideCustomView, + this.onConsoleMessage, super.binaryMessenger, super.instanceManager, }) : super.detached(); @@ -1121,6 +1124,10 @@ class WebChromeClient extends JavaObject { /// mode. final HideCustomViewCallback? onHideCustomView; + /// Report a JavaScript console message to the host application. + final void Function(WebChromeClient instance, ConsoleMessage message)? + onConsoleMessage; + /// Sets the required synchronous return value for the Java method, /// `WebChromeClient.onShowFileChooser(...)`. /// @@ -1150,6 +1157,33 @@ class WebChromeClient extends JavaObject { ); } + /// Sets the required synchronous return value for the Java method, + /// `WebChromeClient.onShowFileChooser(...)`. + /// + /// The Java method, `WebChromeClient.onConsoleMessage(...)`, requires + /// a boolean to be returned and this method sets the returned value for all + /// calls to the Java method. + /// + /// Setting this to true indicates that the client is handling all console + /// messages. + /// + /// Requires [onConsoleMessage] to be nonnull. + /// + /// Defaults to false. + Future setSynchronousReturnValueForOnConsoleMessage( + bool value, + ) { + if (value && onConsoleMessage == null) { + throw StateError( + 'Setting this to true requires `onConsoleMessage` to be nonnull.', + ); + } + return api.setSynchronousReturnValueForOnConsoleMessageFromInstance( + this, + value, + ); + } + @override WebChromeClient copy() { return WebChromeClient.detached( @@ -1160,6 +1194,7 @@ class WebChromeClient extends JavaObject { onGeolocationPermissionsHidePrompt: onGeolocationPermissionsHidePrompt, onShowCustomView: onShowCustomView, onHideCustomView: onHideCustomView, + onConsoleMessage: onConsoleMessage, binaryMessenger: _api.binaryMessenger, instanceManager: _api.instanceManager, ); diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart index 99e1e7f4e104..87f75f19cd37 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart @@ -32,6 +32,42 @@ enum FileChooserMode { save, } +/// Indicates the type of message logged to the console. +/// +/// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel. +enum ConsoleMessageLevel { + /// Indicates a message is logged for debugging. + /// + /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#DEBUG. + debug, + + /// Indicates a message is provided as an error. + /// + /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#ERROR. + error, + + /// Indicates a message is provided as a basic log message. + /// + /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#LOG. + log, + + /// Indicates a message is provided as a tip. + /// + /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#TIP. + tip, + + /// Indicates a message is provided as a warning. + /// + /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#WARNING. + warning, + + /// Indicates a message with an unknown level. + /// + /// This does not represent an actual value provided by the platform and only + /// indicates a value was provided that isn't currently supported. + unknown, +} + class WebResourceRequestData { WebResourceRequestData({ required this.url, @@ -131,6 +167,45 @@ class WebViewPoint { } } +/// Represents a JavaScript console message from WebCore. +/// +/// See https://developer.android.com/reference/android/webkit/ConsoleMessage +class ConsoleMessage { + ConsoleMessage({ + required this.lineNumber, + required this.message, + required this.level, + required this.sourceId, + }); + + int lineNumber; + + String message; + + ConsoleMessageLevel level; + + String sourceId; + + Object encode() { + return [ + lineNumber, + message, + level.index, + sourceId, + ]; + } + + static ConsoleMessage decode(Object result) { + result as List; + return ConsoleMessage( + lineNumber: result[0]! as int, + message: result[1]! as String, + level: ConsoleMessageLevel.values[result[2]! as int], + sourceId: result[3]! as String, + ); + } +} + /// Host API for managing the native `InstanceManager`. class InstanceManagerHostApi { /// Constructor for [InstanceManagerHostApi]. The [binaryMessenger] named argument is @@ -1943,6 +2018,30 @@ class WebChromeClientHostApi { return; } } + + Future setSynchronousReturnValueForOnConsoleMessage( + int arg_instanceId, bool arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_value]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } } class FlutterAssetManagerHostApi { @@ -2012,8 +2111,31 @@ class FlutterAssetManagerHostApi { } } +class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { + const _WebChromeClientFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ConsoleMessage) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ConsoleMessage.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + abstract class WebChromeClientFlutterApi { - static const MessageCodec codec = StandardMessageCodec(); + static const MessageCodec codec = _WebChromeClientFlutterApiCodec(); void onProgressChanged(int instanceId, int webViewInstanceId, int progress); @@ -2037,6 +2159,9 @@ abstract class WebChromeClientFlutterApi { /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsHidePrompt`. void onGeolocationPermissionsHidePrompt(int identifier); + /// Callback to Dart function `WebChromeClient.onConsoleMessage`. + void onConsoleMessage(int instanceId, ConsoleMessage message); + static void setup(WebChromeClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -2210,6 +2335,29 @@ abstract class WebChromeClientFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage was null, expected non-null int.'); + final ConsoleMessage? arg_message = (args[1] as ConsoleMessage?); + assert(arg_message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onConsoleMessage was null, expected non-null ConsoleMessage.'); + api.onConsoleMessage(arg_instanceId!, arg_message!); + return; + }); + } + } } } diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart index 2c773fd9e190..c9191e06f0c0 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -11,7 +11,8 @@ import 'android_webview.dart'; import 'android_webview.g.dart'; import 'instance_manager.dart'; -export 'android_webview.g.dart' show FileChooserMode; +export 'android_webview.g.dart' + show ConsoleMessage, ConsoleMessageLevel, FileChooserMode; /// Converts [WebResourceRequestData] to [WebResourceRequest] WebResourceRequest _toWebResourceRequest(WebResourceRequestData data) { @@ -892,6 +893,17 @@ class WebChromeClientHostApiImpl extends WebChromeClientHostApi { value, ); } + + /// Helper method to convert instances ids to objects. + Future setSynchronousReturnValueForOnConsoleMessageFromInstance( + WebChromeClient instance, + bool value, + ) { + return setSynchronousReturnValueForOnConsoleMessage( + instanceManager.getIdentifier(instance)!, + value, + ); + } } /// Flutter api implementation for [DownloadListener]. @@ -1017,6 +1029,13 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { ); } } + + @override + void onConsoleMessage(int instanceId, ConsoleMessage message) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + instance.onConsoleMessage?.call(instance, message); + } } /// Host api implementation for [WebStorage]. diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart index bc1da74bf952..d0559b4a6e5a 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -13,6 +13,7 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte import 'android_proxy.dart'; import 'android_webview.dart' as android_webview; +import 'android_webview_api_impls.dart'; import 'instance_manager.dart'; import 'platform_views_service_proxy.dart'; import 'weak_reference_utils.dart'; @@ -188,6 +189,42 @@ class AndroidWebViewController extends PlatformWebViewController { }; }, ), + onConsoleMessage: withWeakReferenceTo( + this, + (WeakReference weakReference) { + return (android_webview.WebChromeClient webChromeClient, + android_webview.ConsoleMessage consoleMessage) async { + final void Function(JavaScriptConsoleMessage)? callback = + weakReference.target?._onConsoleLogCallback; + if (callback != null) { + JavaScriptLogLevel logLevel; + switch (consoleMessage.level) { + // Android maps `console.debug` to `MessageLevel.TIP`, it seems + // `MessageLevel.DEBUG` if not being used. + case ConsoleMessageLevel.debug: + case ConsoleMessageLevel.tip: + logLevel = JavaScriptLogLevel.debug; + break; + case ConsoleMessageLevel.error: + logLevel = JavaScriptLogLevel.error; + break; + case ConsoleMessageLevel.warning: + logLevel = JavaScriptLogLevel.warning; + break; + case ConsoleMessageLevel.unknown: + case ConsoleMessageLevel.log: + logLevel = JavaScriptLogLevel.log; + break; + } + + callback(JavaScriptConsoleMessage( + level: logLevel, + message: consoleMessage.message, + )); + } + }; + }, + ), onPermissionRequest: withWeakReferenceTo( this, (WeakReference weakReference) { @@ -255,6 +292,8 @@ class AndroidWebViewController extends PlatformWebViewController { void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback; + void Function(JavaScriptConsoleMessage consoleMessage)? _onConsoleLogCallback; + /// Whether to enable the platform's webview content debugging tools. /// /// Defaults to false. @@ -566,6 +605,18 @@ class AndroidWebViewController extends PlatformWebViewController { _onShowCustomWidgetCallback = onShowCustomWidget; _onHideCustomWidgetCallback = onHideCustomWidget; } + + /// Sets a callback that notifies the host application of any log messages + /// written to the JavaScript console. + @override + Future setOnConsoleMessage( + void Function(JavaScriptConsoleMessage consoleMessage) + onConsoleMessage) async { + _onConsoleLogCallback = onConsoleMessage; + + return _webChromeClient.setSynchronousReturnValueForOnConsoleMessage( + _onConsoleLogCallback != null); + } } /// Android implementation of [PlatformWebViewPermissionRequest]. diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart index c19a2b226b0d..367605395613 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -57,6 +57,42 @@ enum FileChooserMode { save, } +/// Indicates the type of message logged to the console. +/// +/// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel. +enum ConsoleMessageLevel { + /// Indicates a message is logged for debugging. + /// + /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#DEBUG. + debug, + + /// Indicates a message is provided as an error. + /// + /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#ERROR. + error, + + /// Indicates a message is provided as a basic log message. + /// + /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#LOG. + log, + + /// Indicates a message is provided as a tip. + /// + /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#TIP. + tip, + + /// Indicates a message is provided as a warning. + /// + /// See https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel#WARNING. + warning, + + /// Indicates a message with an unknown level. + /// + /// This does not represent an actual value provided by the platform and only + /// indicates a value was provided that isn't currently supported. + unknown, +} + class WebResourceRequestData { WebResourceRequestData( this.url, @@ -89,6 +125,16 @@ class WebViewPoint { int y; } +/// Represents a JavaScript console message from WebCore. +/// +/// See https://developer.android.com/reference/android/webkit/ConsoleMessage +class ConsoleMessage { + late int lineNumber; + late String message; + late ConsoleMessageLevel level; + late String sourceId; +} + /// Handles methods calls to the native Java Object class. /// /// Also handles calls to remove the reference to an instance with `dispose`. @@ -337,6 +383,11 @@ abstract class WebChromeClientHostApi { int instanceId, bool value, ); + + void setSynchronousReturnValueForOnConsoleMessage( + int instanceId, + bool value, + ); } @HostApi(dartHostTestHandler: 'TestAssetManagerHostApi') @@ -379,6 +430,9 @@ abstract class WebChromeClientFlutterApi { /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsHidePrompt`. void onGeolocationPermissionsHidePrompt(int identifier); + + /// Callback to Dart function `WebChromeClient.onConsoleMessage`. + void onConsoleMessage(int instanceId, ConsoleMessage message); } @HostApi(dartHostTestHandler: 'TestWebStorageHostApi') diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 21acae2a2670..8242abd17f05 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.10.1 +version: 3.11.0 environment: sdk: ">=2.19.0 <4.0.0" @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^2.4.0 + webview_flutter_platform_interface: ^2.6.0 dev_dependencies: build_runner: ^2.1.4 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart index 0693ef5d4ce9..269123e081db 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart @@ -520,6 +520,7 @@ class CapturingWebChromeClient extends android_webview.WebChromeClient { super.onShowCustomView, super.onHideCustomView, super.onPermissionRequest, + super.onConsoleMessage, super.binaryMessenger, super.instanceManager, }) : super.detached() { diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index c350be64f2cf..3f3bf73b33cf 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -73,6 +73,9 @@ void main() { android_webview.CustomViewCallback callback)? onShowCustomView, void Function(android_webview.WebChromeClient instance)? onHideCustomView, + void Function(android_webview.WebChromeClient instance, + android_webview.ConsoleMessage message)? + onConsoleMessage, })? createWebChromeClient, android_webview.WebView? mockWebView, android_webview.WebViewClient? mockWebViewClient, @@ -111,6 +114,9 @@ void main() { onShowCustomView, void Function(android_webview.WebChromeClient instance)? onHideCustomView, + void Function(android_webview.WebChromeClient instance, + android_webview.ConsoleMessage message)? + onConsoleMessage, }) => MockWebChromeClient(), createAndroidWebView: () => nonNullMockWebView, @@ -606,6 +612,7 @@ void main() { dynamic onPermissionRequest, dynamic onShowCustomView, dynamic onHideCustomView, + dynamic onConsoleMessage, }) { onShowFileChooserCallback = onShowFileChooser!; return mockWebChromeClient; @@ -676,6 +683,7 @@ void main() { dynamic onPermissionRequest, dynamic onShowCustomView, dynamic onHideCustomView, + dynamic onConsoleMessage, }) { onGeoPermissionHandle = onGeolocationPermissionsShowPrompt!; onGeoPermissionHidePromptHandle = onGeolocationPermissionsHidePrompt!; @@ -750,6 +758,7 @@ void main() { onShowCustomView, void Function(android_webview.WebChromeClient instance)? onHideCustomView, + dynamic onConsoleMessage, }) { onShowCustomViewHandle = onShowCustomView!; onHideCustomViewHandle = onHideCustomView!; @@ -802,6 +811,7 @@ void main() { )? onPermissionRequest, dynamic onShowCustomView, dynamic onHideCustomView, + dynamic onConsoleMessage, }) { onPermissionRequestCallback = onPermissionRequest!; return mockWebChromeClient; @@ -856,6 +866,7 @@ void main() { )? onPermissionRequest, dynamic onShowCustomView, dynamic onHideCustomView, + dynamic onConsoleMessage, }) { onPermissionRequestCallback = onPermissionRequest!; return mockWebChromeClient; @@ -881,6 +892,104 @@ void main() { expect(callbackCalled, isFalse); }); + test('setOnConsoleLogCallback', () async { + late final void Function( + android_webview.WebChromeClient instance, + android_webview.ConsoleMessage message, + ) onConsoleMessageCallback; + + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + dynamic onShowFileChooser, + dynamic onGeolocationPermissionsShowPrompt, + dynamic onGeolocationPermissionsHidePrompt, + dynamic onPermissionRequest, + dynamic onShowCustomView, + dynamic onHideCustomView, + void Function( + android_webview.WebChromeClient, + android_webview.ConsoleMessage, + )? onConsoleMessage, + }) { + onConsoleMessageCallback = onConsoleMessage!; + return mockWebChromeClient; + }, + ); + + final Map logs = + {}; + await controller.setOnConsoleMessage( + (JavaScriptConsoleMessage message) async { + logs[message.message] = message.level; + }, + ); + + onConsoleMessageCallback( + mockWebChromeClient, + ConsoleMessage( + lineNumber: 42, + message: 'Debug message', + level: ConsoleMessageLevel.debug, + sourceId: 'source', + ), + ); + onConsoleMessageCallback( + mockWebChromeClient, + ConsoleMessage( + lineNumber: 42, + message: 'Error message', + level: ConsoleMessageLevel.error, + sourceId: 'source', + ), + ); + onConsoleMessageCallback( + mockWebChromeClient, + ConsoleMessage( + lineNumber: 42, + message: 'Log message', + level: ConsoleMessageLevel.log, + sourceId: 'source', + ), + ); + onConsoleMessageCallback( + mockWebChromeClient, + ConsoleMessage( + lineNumber: 42, + message: 'Tip message', + level: ConsoleMessageLevel.tip, + sourceId: 'source', + ), + ); + onConsoleMessageCallback( + mockWebChromeClient, + ConsoleMessage( + lineNumber: 42, + message: 'Warning message', + level: ConsoleMessageLevel.warning, + sourceId: 'source', + ), + ); + onConsoleMessageCallback( + mockWebChromeClient, + ConsoleMessage( + lineNumber: 42, + message: 'Unknown message', + level: ConsoleMessageLevel.unknown, + sourceId: 'source', + ), + ); + + expect(logs.length, 6); + expect(logs['Debug message'], JavaScriptLogLevel.debug); + expect(logs['Error message'], JavaScriptLogLevel.error); + expect(logs['Log message'], JavaScriptLogLevel.log); + expect(logs['Tip message'], JavaScriptLogLevel.debug); + expect(logs['Warning message'], JavaScriptLogLevel.warning); + expect(logs['Unknown message'], JavaScriptLogLevel.log); + }); + test('runJavaScript', () async { final MockWebView mockWebView = MockWebView(); final AndroidWebViewController controller = createControllerWithMocks( @@ -1334,6 +1443,7 @@ void main() { android_webview.CustomViewCallback callback)? onShowCustomView, dynamic onHideCustomView, + dynamic onConsoleMessage, }) { onShowCustomViewCallback = onShowCustomView; return mockWebChromeClient; diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart index b6d6e2c574a8..98d78795f3c3 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart @@ -739,8 +739,8 @@ class MockAndroidWebViewController extends _i1.Mock ) as _i9.Future); @override _i9.Future setCustomWidgetCallbacks({ - _i8.OnShowCustomWidgetCallback? onShowCustomWidget, - _i8.OnHideCustomWidgetCallback? onHideCustomWidget, + required _i8.OnShowCustomWidgetCallback? onShowCustomWidget, + required _i8.OnHideCustomWidgetCallback? onHideCustomWidget, }) => (super.noSuchMethod( Invocation.method( @@ -754,6 +754,26 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i9.Future.value(), returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); + @override + _i9.Future setOnConsoleMessage( + void Function(_i3.JavaScriptConsoleMessage)? onConsoleMessage) => + (super.noSuchMethod( + Invocation.method( + #setOnConsoleMessage, + [onConsoleMessage], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future getUserAgent() => (super.noSuchMethod( + Invocation.method( + #getUserAgent, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [AndroidWebViewProxy]. @@ -775,6 +795,10 @@ class MockAndroidWebViewProxy extends _i1.Mock ) as _i2.WebView Function()); @override _i2.WebChromeClient Function({ + void Function( + _i2.WebChromeClient, + _i2.ConsoleMessage, + )? onConsoleMessage, void Function(_i2.WebChromeClient)? onGeolocationPermissionsHidePrompt, _i9.Future Function( String, @@ -801,6 +825,10 @@ class MockAndroidWebViewProxy extends _i1.Mock }) get createAndroidWebChromeClient => (super.noSuchMethod( Invocation.getter(#createAndroidWebChromeClient), returnValue: ({ + void Function( + _i2.WebChromeClient, + _i2.ConsoleMessage, + )? onConsoleMessage, void Function(_i2.WebChromeClient)? onGeolocationPermissionsHidePrompt, _i9.Future Function( @@ -831,6 +859,10 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#createAndroidWebChromeClient), ), returnValueForMissingStub: ({ + void Function( + _i2.WebChromeClient, + _i2.ConsoleMessage, + )? onConsoleMessage, void Function(_i2.WebChromeClient)? onGeolocationPermissionsHidePrompt, _i9.Future Function( @@ -861,6 +893,10 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#createAndroidWebChromeClient), ), ) as _i2.WebChromeClient Function({ + void Function( + _i2.WebChromeClient, + _i2.ConsoleMessage, + )? onConsoleMessage, void Function(_i2.WebChromeClient)? onGeolocationPermissionsHidePrompt, _i9.Future Function( String, @@ -1780,6 +1816,16 @@ class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); @override + _i9.Future setSynchronousReturnValueForOnConsoleMessage(bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnConsoleMessage, + [value], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override _i2.WebChromeClient copy() => (super.noSuchMethod( Invocation.method( #copy, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart index b23bcaa24a2a..5213793ee9e9 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart @@ -475,8 +475,8 @@ class MockAndroidWebViewController extends _i1.Mock ) as _i5.Future); @override _i5.Future setCustomWidgetCallbacks({ - _i6.OnShowCustomWidgetCallback? onShowCustomWidget, - _i6.OnHideCustomWidgetCallback? onHideCustomWidget, + required _i6.OnShowCustomWidgetCallback? onShowCustomWidget, + required _i6.OnHideCustomWidgetCallback? onHideCustomWidget, }) => (super.noSuchMethod( Invocation.method( @@ -490,6 +490,25 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + @override + _i5.Future setOnConsoleMessage( + void Function(_i3.JavaScriptConsoleMessage)? onConsoleMessage) => + (super.noSuchMethod( + Invocation.method( + #setOnConsoleMessage, + [onConsoleMessage], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getUserAgent() => (super.noSuchMethod( + Invocation.method( + #getUserAgent, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TestInstanceManagerHostApi]. diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart index da03052c7f22..2629d5665a2d 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -1120,6 +1120,81 @@ void main() { expect(callbackParameters, [instance]); }); + test('onConsoleMessage', () async { + late final List result; + when(mockWebChromeClient.onConsoleMessage).thenReturn( + (WebChromeClient instance, ConsoleMessage message) { + result = [instance, message]; + }, + ); + + final ConsoleMessage message = ConsoleMessage( + lineNumber: 0, + message: 'message', + level: ConsoleMessageLevel.error, + sourceId: 'sourceId', + ); + + flutterApi.onConsoleMessage( + mockWebChromeClientInstanceId, + message, + ); + expect(result[0], mockWebChromeClient); + expect(result[1], message); + }); + + test('setSynchronousReturnValueForOnConsoleMessage', () { + final MockTestWebChromeClientHostApi mockHostApi = + MockTestWebChromeClientHostApi(); + TestWebChromeClientHostApi.setup(mockHostApi); + + WebChromeClient.api = + WebChromeClientHostApiImpl(instanceManager: instanceManager); + + final WebChromeClient webChromeClient = WebChromeClient.detached(); + instanceManager.addHostCreatedInstance(webChromeClient, 2); + + webChromeClient.setSynchronousReturnValueForOnConsoleMessage(false); + + verify( + mockHostApi.setSynchronousReturnValueForOnConsoleMessage(2, false), + ); + }); + + test( + 'setSynchronousReturnValueForOnConsoleMessage throws StateError when onConsoleMessage is null', + () { + final MockTestWebChromeClientHostApi mockHostApi = + MockTestWebChromeClientHostApi(); + TestWebChromeClientHostApi.setup(mockHostApi); + + WebChromeClient.api = + WebChromeClientHostApiImpl(instanceManager: instanceManager); + + final WebChromeClient clientWithNullCallback = + WebChromeClient.detached(); + instanceManager.addHostCreatedInstance(clientWithNullCallback, 2); + + expect( + () => clientWithNullCallback + .setSynchronousReturnValueForOnConsoleMessage(true), + throwsStateError, + ); + + final WebChromeClient clientWithNonnullCallback = + WebChromeClient.detached( + onConsoleMessage: (_, __) async {}, + ); + instanceManager.addHostCreatedInstance(clientWithNonnullCallback, 3); + + clientWithNonnullCallback + .setSynchronousReturnValueForOnConsoleMessage(true); + + verify( + mockHostApi.setSynchronousReturnValueForOnConsoleMessage(3, true), + ); + }); + test('copy', () { expect(WebChromeClient.detached().copy(), isA()); }); diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart index fecf8140a114..717cc14b7bd2 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -480,6 +480,21 @@ class MockTestWebChromeClientHostApi extends _i1.Mock ), returnValueForMissingStub: null, ); + @override + void setSynchronousReturnValueForOnConsoleMessage( + int? instanceId, + bool? value, + ) => + super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnConsoleMessage, + [ + instanceId, + value, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebSettingsHostApi]. @@ -1181,6 +1196,16 @@ class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); @override + _i5.Future setSynchronousReturnValueForOnConsoleMessage(bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnConsoleMessage, + [value], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override _i2.WebChromeClient copy() => (super.noSuchMethod( Invocation.method( #copy, diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart index c52703812fe6..150ea5a275d5 100644 --- a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart @@ -780,6 +780,16 @@ class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); @override + _i5.Future setSynchronousReturnValueForOnConsoleMessage(bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnConsoleMessage, + [value], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override _i2.WebChromeClient copy() => (super.noSuchMethod( Invocation.method( #copy, diff --git a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart index b88ea8a8c2dd..a92c054268b7 100644 --- a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart @@ -1516,6 +1516,8 @@ abstract class TestWebChromeClientHostApi { void setSynchronousReturnValueForOnShowFileChooser( int instanceId, bool value); + void setSynchronousReturnValueForOnConsoleMessage(int instanceId, bool value); + static void setup(TestWebChromeClientHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1568,6 +1570,33 @@ abstract class TestWebChromeClientHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage was null, expected non-null int.'); + final bool? arg_value = (args[1] as bool?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage was null, expected non-null bool.'); + api.setSynchronousReturnValueForOnConsoleMessage( + arg_instanceId!, arg_value!); + return []; + }); + } + } } }