diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 0b2e4dfa9b29..d6fcbb29b5d4 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.3.0 + +* Adds support to register a callback to receive JavaScript console messages. See `WebViewController.setConsoleLogCallback`. + ## 4.2.2 * Fixes documentation typo. diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index c7fbd63958fd..96cbbf8f7e36 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -76,6 +76,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}); @@ -208,6 +242,7 @@ enum MenuOptions { loadHtmlString, transparentBackground, setCookie, + logExample, } class SampleMenu extends StatelessWidget { @@ -264,6 +299,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.setCookie: _onSetCookie(); break; + case MenuOptions.logExample: + _onLogExample(); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -320,6 +358,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.setCookie, child: Text('Set cookie'), ), + const PopupMenuItem( + value: MenuOptions.logExample, + child: Text('Log example'), + ), ], ); } @@ -463,6 +505,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/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index 291fc794e4f7..88f1d89e58eb 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -36,3 +36,8 @@ flutter: - assets/sample_video.mp4 - assets/www/index.html - assets/www/styles/style.css + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {webview_flutter: {path: ../../../webview_flutter/webview_flutter}, webview_flutter_android: {path: ../../../webview_flutter/webview_flutter_android}, webview_flutter_platform_interface: {path: ../../../webview_flutter/webview_flutter_platform_interface}, webview_flutter_wkwebview: {path: ../../../webview_flutter/webview_flutter_wkwebview}} diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart index f8e4c411a8e0..b54a004bb7ca 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart @@ -353,6 +353,22 @@ class WebViewController { Future setUserAgent(String? userAgent) { return platform.setUserAgent(userAgent); } + + /// Sets a callback that notifies the host application on any log messages + /// written to the JavaScript console. + /// + /// Platforms may not preserve all the log level information so clients should + /// not rely on a 1:1 mapping between the JavaScript calls. + /// + /// On iOS setting this callback will inject a custom [WKUserScript] which + /// overrides the default implementation of `console.debug`, `console.error`, + /// `console.info`, `console.log` and `console.warning` methods. The iOS + /// WebKit framework unfortunately doesn't provide a build in method to + /// forward console messages. + Future setOnConsoleMessage( + void Function(JavaScriptConsoleMessage message) onConsoleMessage) { + return platform.setOnConsoleMessage(onConsoleMessage); + } } /// Permissions request when web content requests access to protected resources. diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart index 3e85cc389ac0..37dde9dd3125 100644 --- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -6,6 +6,7 @@ library webview_flutter; export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' show + JavaScriptConsoleMessage, JavaScriptMessage, JavaScriptMode, LoadRequestMethod, diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 6dae99f307ce..baf63f6bdae5 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/packages/tree/main/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: 4.2.2 +version: 4.3.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -29,3 +29,8 @@ dev_dependencies: sdk: flutter mockito: 5.4.1 plugin_platform_interface: ^2.1.3 + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {webview_flutter_android: {path: ../../webview_flutter/webview_flutter_android}, webview_flutter_platform_interface: {path: ../../webview_flutter/webview_flutter_platform_interface}, webview_flutter_wkwebview: {path: ../../webview_flutter/webview_flutter_wkwebview}} diff --git a/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart index ae59ec3b5a73..ade55a8affef 100644 --- a/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter/test/legacy/webview_flutter_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i9; diff --git a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart index f290d4036c27..0a3475f8fcf1 100644 --- a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter/test/navigation_delegate_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i8; @@ -208,6 +210,16 @@ class MockPlatformNavigationDelegate extends _i1.Mock returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); @override + _i8.Future setOnHttpError(_i3.HttpResponseErrorCallback? onHttpError) => + (super.noSuchMethod( + Invocation.method( + #setOnHttpError, + [onHttpError], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override _i8.Future setOnProgress(_i3.ProgressCallback? onProgress) => (super.noSuchMethod( Invocation.method( diff --git a/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart b/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart index 79d01ba04451..8f7535c28434 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart @@ -387,6 +387,20 @@ void main() { requestCallback(const TestPlatformWebViewPermissionRequest()); expect(permissionRequestCallbackCalled, isTrue); }); + + test('setConsoleLogCallback', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + void onConsoleMessage(JavaScriptConsoleMessage message) {} + + await webViewController.setOnConsoleMessage(onConsoleMessage); + + verify(mockPlatformWebViewController.setOnConsoleMessage(onConsoleMessage)); + }); } class TestPlatformWebViewPermissionRequest diff --git a/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart index fc242755fd1a..b919b5f70eb3 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter/test/webview_controller_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; import 'dart:ui' as _i3; @@ -353,6 +355,17 @@ class MockPlatformWebViewController extends _i1.Mock returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + @override + _i5.Future setOnConsoleMessage( + void Function(_i2.JavaScriptConsoleMessage)? onConsoleMessage) => + (super.noSuchMethod( + Invocation.method( + #setOnConsoleMessage, + [onConsoleMessage], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [PlatformNavigationDelegate]. @@ -405,6 +418,16 @@ class MockPlatformNavigationDelegate extends _i1.Mock returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); @override + _i5.Future setOnHttpError(_i6.HttpResponseErrorCallback? onHttpError) => + (super.noSuchMethod( + Invocation.method( + #setOnHttpError, + [onHttpError], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override _i5.Future setOnProgress(_i6.ProgressCallback? onProgress) => (super.noSuchMethod( Invocation.method( diff --git a/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart index 1d7de302fe62..858b2cf8876c 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter/test/webview_cookie_manager_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i4; diff --git a/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart index 9d3a79066865..f7fbd3c9dfad 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter/test/webview_widget_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i7; import 'dart:ui' as _i3; @@ -371,6 +373,17 @@ class MockPlatformWebViewController extends _i1.Mock returnValue: _i7.Future.value(), returnValueForMissingStub: _i7.Future.value(), ) as _i7.Future); + @override + _i7.Future setOnConsoleMessage( + void Function(_i2.JavaScriptConsoleMessage)? onConsoleMessage) => + (super.noSuchMethod( + Invocation.method( + #setOnConsoleMessage, + [onConsoleMessage], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); } /// A class which mocks [PlatformWebViewWidget]. diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 03f1539a069b..7346be8997b0 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.10.0 + +* Adds support to register a callback to receive JavaScript console messages. See `AndroidWebViewController.setConsoleLogCallback`. + ## 3.9.2 * Fixes bug where `PlatformWebViewWidget` doesn't rebuild when the controller or PlatformView 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 cc52b0b10888..eb7f8d8a6875 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 FileChooserModeEnumData { private @NonNull FileChooserMode value; @@ -458,6 +514,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(level == null ? null : ConsoleMessageLevel.values()[(int) level]); + Object sourceId = list.get(3); + pigeonResult.setSourceId((String) sourceId); + return pigeonResult; + } + } + public interface Result { @SuppressWarnings("UnknownNullness") void success(T result); @@ -2392,6 +2578,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(); @@ -2452,6 +2641,33 @@ static void setup( channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.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. */ @@ -2523,6 +2739,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; @@ -2538,7 +2782,7 @@ public interface Reply { } /** The codec used by WebChromeClientFlutterApi. */ static @NonNull MessageCodec getCodec() { - return new StandardMessageCodec(); + return WebChromeClientFlutterApiCodec.INSTANCE; } public void onProgressChanged( @@ -2616,6 +2860,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.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 ad5168fa110a..ad7fb3dd803e 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 @@ -5,6 +5,7 @@ package io.flutter.plugins.webviewflutter; import android.os.Build; +import android.webkit.ConsoleMessage; import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.WebChromeClient; @@ -26,6 +27,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. * @@ -117,6 +136,21 @@ public void onPermissionRequest( callback); } + 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 74ea45e5359a..a14b7f430518 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 @@ -7,6 +7,7 @@ import android.net.Uri; import android.os.Build; import android.os.Message; +import android.webkit.ConsoleMessage; import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.ValueCallback; @@ -37,6 +38,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. @@ -95,10 +97,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; + } } /** @@ -234,4 +247,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 e2d5a444b716..1dd25c199ea9 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 @@ -15,6 +15,7 @@ import android.net.Uri; import android.os.Message; +import android.webkit.ConsoleMessage; import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.WebResourceRequest; @@ -141,4 +142,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 53b8a0078fd0..a5798ff4a80d 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 @@ -1226,6 +1226,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 0c3e3411bd09..d187a9454b3b 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}); @@ -201,6 +235,7 @@ enum MenuOptions { loadHtmlString, transparentBackground, setCookie, + logExample, } class SampleMenu extends StatelessWidget { @@ -261,6 +296,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.setCookie: _onSetCookie(); break; + case MenuOptions.logExample: + _onLogExample(); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -317,6 +355,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.transparentBackground, child: Text('Transparent background example'), ), + const PopupMenuItem( + value: MenuOptions.logExample, + child: Text('Log example'), + ), ], ); } @@ -465,6 +507,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 0b23ead3a90c..94d5d4028d69 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -33,3 +33,8 @@ flutter: - assets/sample_video.mp4 - assets/www/index.html - assets/www/styles/style.css + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {webview_flutter_android: {path: ../../../webview_flutter/webview_flutter_android}, webview_flutter_platform_interface: {path: ../../../webview_flutter/webview_flutter_platform_interface}} 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 e95de8cfa6f1..d40af9c03e72 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 @@ -28,22 +28,26 @@ class AndroidWebViewProxy { final android_webview.WebView Function() createAndroidWebView; /// Constructs a [android_webview.WebChromeClient]. - final android_webview.WebChromeClient Function( - {void Function(android_webview.WebView webView, int progress)? - onProgressChanged, - Future> Function( - android_webview.WebView webView, - android_webview.FileChooserParams params, - )? onShowFileChooser, - void Function( - android_webview.WebChromeClient instance, - android_webview.PermissionRequest request, - )? onPermissionRequest, - Future Function(String origin, - android_webview.GeolocationPermissionsCallback callback)? - onGeolocationPermissionsShowPrompt, - void Function(android_webview.WebChromeClient instance)? - onGeolocationPermissionsHidePrompt}) createAndroidWebChromeClient; + final android_webview.WebChromeClient Function({ + void Function(android_webview.WebView webView, int progress)? + onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + Future Function(String origin, + android_webview.GeolocationPermissionsCallback callback)? + onGeolocationPermissionsShowPrompt, + void Function(android_webview.WebChromeClient instance)? + onGeolocationPermissionsHidePrompt, + void Function(android_webview.WebChromeClient instance, + android_webview.ConsoleMessage message)? + onConsoleMessage, + }) createAndroidWebChromeClient; /// Constructs a [android_webview.WebViewClient]. final android_webview.WebViewClient Function({ 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 85ec6b902c57..5f37fd2a24f2 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. /// @@ -1033,6 +1034,7 @@ class WebChromeClient extends JavaObject { this.onPermissionRequest, this.onGeolocationPermissionsShowPrompt, this.onGeolocationPermissionsHidePrompt, + this.onConsoleMessage, @visibleForTesting super.binaryMessenger, @visibleForTesting super.instanceManager, }) : super.detached() { @@ -1052,6 +1054,7 @@ class WebChromeClient extends JavaObject { this.onPermissionRequest, this.onGeolocationPermissionsShowPrompt, this.onGeolocationPermissionsHidePrompt, + this.onConsoleMessage, super.binaryMessenger, super.instanceManager, }) : super.detached(); @@ -1096,6 +1099,10 @@ class WebChromeClient extends JavaObject { WebChromeClient instance, )? onGeolocationPermissionsHidePrompt; + /// 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(...)`. /// @@ -1125,6 +1132,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( @@ -1132,6 +1166,7 @@ class WebChromeClient extends JavaObject { onShowFileChooser: onShowFileChooser, onGeolocationPermissionsShowPrompt: onGeolocationPermissionsShowPrompt, onGeolocationPermissionsHidePrompt: onGeolocationPermissionsHidePrompt, + 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 70473bfc13cf..5843b2d97605 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 FileChooserModeEnumData { FileChooserModeEnumData({ required this.value, @@ -152,6 +188,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 @@ -377,7 +452,6 @@ class CookieManagerHostApi { class _WebViewHostApiCodec extends StandardMessageCodec { const _WebViewHostApiCodec(); - @override void writeValue(WriteBuffer buffer, Object? value) { if (value is WebViewPoint) { @@ -1529,7 +1603,6 @@ class WebViewClientHostApi { class _WebViewClientFlutterApiCodec extends StandardMessageCodec { const _WebViewClientFlutterApiCodec(); - @override void writeValue(WriteBuffer buffer, Object? value) { if (value is WebResourceErrorData) { @@ -1913,6 +1986,30 @@ class WebChromeClientHostApi { return; } } + + Future setSynchronousReturnValueForOnConsoleMessage( + int arg_instanceId, bool arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.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 { @@ -1981,8 +2078,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); @@ -1999,6 +2119,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}) { { @@ -2125,6 +2248,29 @@ abstract class WebChromeClientFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.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.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.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.WebChromeClientFlutterApi.onConsoleMessage was null, expected non-null ConsoleMessage.'); + api.onConsoleMessage(arg_instanceId!, arg_message!); + return; + }); + } + } } } @@ -2185,7 +2331,6 @@ class WebStorageHostApi { class _FileChooserParamsFlutterApiCodec extends StandardMessageCodec { const _FileChooserParamsFlutterApiCodec(); - @override void writeValue(WriteBuffer buffer, Object? value) { if (value is FileChooserModeEnumData) { 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 74e116da5b90..8f076f5b9960 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) { @@ -879,6 +880,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]. @@ -976,6 +988,13 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { request.deny(); } } + + @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 3761dd2b99df..d18366021f1d 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 @@ -11,6 +11,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'; @@ -151,6 +152,40 @@ class AndroidWebViewController extends PlatformWebViewController { }; }, ), + onConsoleMessage: withWeakReferenceTo( + this, + (WeakReference weakReference) { + return (android_webview.WebChromeClient webChromeClient, + android_webview.ConsoleMessage consoleMessage) async { + if (weakReference.target?._onConsoleLogCallback != 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; + } + + _onConsoleLogCallback!(JavaScriptConsoleMessage( + level: logLevel, + message: consoleMessage.message, + )); + } + }; + }, + ), onPermissionRequest: withWeakReferenceTo( this, (WeakReference weakReference) { @@ -214,6 +249,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. @@ -495,6 +532,18 @@ class AndroidWebViewController extends PlatformWebViewController { _onGeolocationPermissionsShowPrompt = onShowPrompt; _onGeolocationPermissionsHidePrompt = onHidePrompt; } + + /// 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 f75eb3235bde..84c66568d66e 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -63,6 +63,42 @@ class FileChooserModeEnumData { late FileChooserMode value; } +/// 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, @@ -95,6 +131,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`. @@ -343,6 +389,11 @@ abstract class WebChromeClientHostApi { int instanceId, bool value, ); + + void setSynchronousReturnValueForOnConsoleMessage( + int instanceId, + bool value, + ); } @HostApi(dartHostTestHandler: 'TestAssetManagerHostApi') @@ -375,6 +426,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 f56b5a6a9e7e..7a88fe63acef 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.9.2 +version: 3.10.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -28,3 +28,8 @@ dev_dependencies: sdk: flutter mockito: 5.4.1 pigeon: ^9.2.4 + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {webview_flutter_platform_interface: {path: ../../webview_flutter/webview_flutter_platform_interface}} 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 3f93d46b7705..57f81b7b357b 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 @@ -518,6 +518,7 @@ class CapturingWebChromeClient extends android_webview.WebChromeClient { super.onGeolocationPermissionsShowPrompt, super.onGeolocationPermissionsHidePrompt, super.onPermissionRequest, + super.onConsoleMessage, super.binaryMessenger, super.instanceManager, }) : super.detached() { diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.mocks.dart index aa0bf6569d8d..37eaec813c02 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/android_navigation_delegate_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; 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 f3bdb63322e9..925027aa40d4 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 @@ -65,6 +65,9 @@ void main() { android_webview.WebChromeClient instance, android_webview.PermissionRequest request, )? onPermissionRequest, + void Function(android_webview.WebChromeClient instance, + android_webview.ConsoleMessage message)? + onConsoleMessage, })? createWebChromeClient, android_webview.WebView? mockWebView, android_webview.WebViewClient? mockWebViewClient, @@ -97,7 +100,11 @@ void main() { )? onGeolocationPermissionsShowPrompt, void Function( android_webview.WebChromeClient instance)? - onGeolocationPermissionsHidePrompt}) => + onGeolocationPermissionsHidePrompt, + void Function( + android_webview.WebChromeClient instance, + android_webview.ConsoleMessage message)? + onConsoleMessage}) => MockWebChromeClient(), createAndroidWebView: () => nonNullMockWebView, createAndroidWebViewClient: ({ @@ -590,6 +597,7 @@ void main() { dynamic onGeolocationPermissionsShowPrompt, dynamic onGeolocationPermissionsHidePrompt, dynamic onPermissionRequest, + dynamic onConsoleMessage, }) { onShowFileChooserCallback = onShowFileChooser!; return mockWebChromeClient; @@ -658,6 +666,7 @@ void main() { void Function(android_webview.WebChromeClient instance)? onGeolocationPermissionsHidePrompt, dynamic onPermissionRequest, + dynamic onConsoleMessage, }) { onGeoPermissionHandle = onGeolocationPermissionsShowPrompt!; onGeoPermissionHidePromptHandle = onGeolocationPermissionsHidePrompt!; @@ -710,6 +719,7 @@ void main() { android_webview.WebChromeClient instance, android_webview.PermissionRequest request, )? onPermissionRequest, + dynamic onConsoleMessage, }) { onPermissionRequestCallback = onPermissionRequest!; return mockWebChromeClient; @@ -762,6 +772,7 @@ void main() { android_webview.WebChromeClient instance, android_webview.PermissionRequest request, )? onPermissionRequest, + dynamic onConsoleMessage, }) { onPermissionRequestCallback = onPermissionRequest!; return mockWebChromeClient; @@ -787,6 +798,102 @@ 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, + 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( 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 f6e7124c4d95..012fab507560 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 @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/android_webview_controller_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i9; import 'dart:typed_data' as _i14; @@ -736,6 +738,17 @@ 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); } /// A class which mocks [AndroidWebViewProxy]. @@ -757,6 +770,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, @@ -777,6 +794,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( @@ -801,6 +822,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( @@ -825,6 +850,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, @@ -1738,6 +1767,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 ee0b48188ef6..b53db0111197 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 @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/android_webview_cookie_manager_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; import 'dart:ui' as _i4; @@ -471,6 +473,17 @@ 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); } /// 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 995694fee029..5adbeaba30c4 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 @@ -1037,6 +1037,81 @@ void main() { expect(callbackParameters, [instance, request]); }); + 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 a7f825fa16c8..73fad329046a 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 @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/android_webview_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; import 'dart:typed_data' as _i7; @@ -459,6 +461,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]. @@ -1160,6 +1177,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/instance_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.mocks.dart index c9e6e4bfadac..d071991ab0c4 100644 --- a/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/instance_manager_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/instance_manager_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart index 864dbaa04aa0..df06f6252274 100644 --- a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/legacy/webview_android_cookie_manager_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; 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 b82f8dea91e6..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 @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_android/test/legacy/webview_android_widget_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; import 'dart:typed_data' as _i6; @@ -778,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 98060e042f9b..d3a7bb7b3d0d 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 @@ -217,7 +217,6 @@ abstract class TestCookieManagerHostApi { class _TestWebViewHostApiCodec extends StandardMessageCodec { const _TestWebViewHostApiCodec(); - @override void writeValue(WriteBuffer buffer, Object? value) { if (value is WebViewPoint) { @@ -1474,6 +1473,8 @@ abstract class TestWebChromeClientHostApi { void setSynchronousReturnValueForOnShowFileChooser( int instanceId, bool value); + void setSynchronousReturnValueForOnConsoleMessage(int instanceId, bool value); + static void setup(TestWebChromeClientHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1525,6 +1526,33 @@ abstract class TestWebChromeClientHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.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.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.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.WebChromeClientHostApi.setSynchronousReturnValueForOnConsoleMessage was null, expected non-null bool.'); + api.setSynchronousReturnValueForOnConsoleMessage( + arg_instanceId!, arg_value!); + return []; + }); + } + } } } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 30c3ab2a8d66..c6142d5a5461 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.5.0 + +* Adds support to register a callback to intercept messages that are written to + the JavaScript console. See `PlatformWebViewController.setConsoleLogCallback`. + ## 2.4.0 * Adds support to retrieve the url from a web resource loading error. See `WebResourceError.url`. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart index c7ec71e5dcc2..402b849af789 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart @@ -270,6 +270,15 @@ abstract class PlatformWebViewController extends PlatformInterface { 'setOnPermissionRequest is not implemented on the current platform', ); } + + /// Sets a callback that notifies the host application of any console messages + /// written to the JavaScript console. + Future setOnConsoleMessage( + void Function(JavaScriptConsoleMessage consoleMessage) onConsoleMessage) { + throw UnimplementedError( + 'setConsoleLogCallback is not implemented on the current platform', + ); + } } /// Describes the parameters necessary for registering a JavaScript channel. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_console_message.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_console_message.dart new file mode 100644 index 000000000000..3509e685f4f6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_console_message.dart @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; + +import 'javascript_log_level.dart'; + +/// Represents a console message written to the JavaScript console. +@immutable +class JavaScriptConsoleMessage { + /// Creates a [JavaScriptConsoleMessage]. + const JavaScriptConsoleMessage({ + required this.level, + required this.message, + }); + + /// The severity of a JavaScript log message. + final JavaScriptLogLevel level; + + /// The message written to the console. + final String message; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_log_level.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_log_level.dart new file mode 100644 index 000000000000..b7319c9082f0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_log_level.dart @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Represents the severity of a JavaScript log message. +enum JavaScriptLogLevel { + /// Indicates an error message was logged via an "error" event of the + /// `console.error` method. + error, + + /// Indicates a warning message was logged using the `console.warning` + /// method. + warning, + + /// Indicates a debug message was logged using the `console.debug` method. + debug, + + /// Indicates an informational message was logged using the `console.info` + /// method. + info, + + /// Indicates a log message was logged using the `console.log` method. + log, +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart index e980aece3afa..c6cbe6967a6c 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart @@ -3,6 +3,8 @@ // found in the LICENSE file. export 'http_response_error.dart'; +export 'javascript_console_message.dart'; +export 'javascript_log_level.dart'; export 'javascript_message.dart'; export 'javascript_mode.dart'; export 'load_request_params.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index eca7b4927be3..9d01da1fb318 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/webview_flutt issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.4.0 +version: 2.5.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart index 2d808dceb69d..3e5d321c3d62 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart @@ -387,6 +387,20 @@ void main() { throwsUnimplementedError, ); }); + + test( + 'Default implementation of setConsoleLogCallback should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => + controller.setOnConsoleMessage((JavaScriptConsoleMessage message) {}), + throwsUnimplementedError, + ); + }); } class MockWebViewPlatformWithMixin extends MockWebViewPlatform diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart index 1268858978c8..3ab612ee8c51 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_platform_interface/test/platform_webview_controller_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i4; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.mocks.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.mocks.dart index e9ff0bd9f282..7b6d661a4c29 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_platform_interface/test/webview_platform_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart' diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 35b2b8fb0afa..08fdde3c746e 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.8.0 + +* Adds support to register a callback to receive JavaScript console messages. See `WebKitWebViewController.setConsoleLogCallback`. + ## 3.7.2 * Fixes bug where `PlatformWebViewWidget` doesn't rebuild when the controller changes. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart index edbe5e366fee..d1ae8a8c952b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -1193,6 +1193,48 @@ Future main() async { await expectLater(controller.currentUrl(), completion(primaryUrl)); }, ); + + 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 consoleMessage) { + debugMessageReceived + .complete('${consoleMessage.level.name}:${consoleMessage.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_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index 7367828dbedf..4180650574db 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -74,6 +74,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 { loadHtmlString, transparentBackground, setCookie, + logExample, } class SampleMenu extends StatelessWidget { @@ -262,6 +297,9 @@ class SampleMenu extends StatelessWidget { case MenuOptions.setCookie: _onSetCookie(); break; + case MenuOptions.logExample: + _onLogExample(); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -318,6 +356,10 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.transparentBackground, child: Text('Transparent background example'), ), + const PopupMenuItem( + value: MenuOptions.logExample, + child: Text('Log example'), + ), ], ); } @@ -466,6 +508,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_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index aae7c82df047..c23c0023efd6 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -32,3 +32,8 @@ flutter: - assets/sample_video.mp4 - assets/www/index.html - assets/www/styles/style.css + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {webview_flutter_platform_interface: {path: ../../../webview_flutter/webview_flutter_platform_interface}, webview_flutter_wkwebview: {path: ../../../webview_flutter/webview_flutter_wkwebview}} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index 5b87a39fe0bf..7db3a03aa148 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; @@ -269,6 +270,7 @@ class WebKitWebViewController extends PlatformWebViewController { bool _zoomEnabled = true; WebKitNavigationDelegate? _currentNavigationDelegate; + void Function(JavaScriptConsoleMessage)? _onConsoleMessageCallback; void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback; WebKitWebViewControllerCreationParams get _webKitParams => @@ -327,7 +329,8 @@ class WebKitWebViewController extends PlatformWebViewController { javaScriptChannelParams is WebKitJavaScriptChannelParams ? javaScriptChannelParams : WebKitJavaScriptChannelParams.fromJavaScriptChannelParams( - javaScriptChannelParams); + javaScriptChannelParams, + ); _javaScriptChannelParams[webKitParams.name] = webKitParams; @@ -512,6 +515,104 @@ class WebKitWebViewController extends PlatformWebViewController { .addUserScript(userScript); } + /// Sets a callback that notifies the host application of any log messages + /// written to the JavaScript console. + /// + /// Because the iOS WKWebView doesn't provide a built-in way to access the + /// console, setting this callback will inject a custom [WKUserScript] which + /// overrides the JavaScript `console.debug`, `console.error`, `console.info`, + /// `console.log` and `console.warn` methods and forwards the console message + /// via a `JavaScriptChannel` to the host application. + @override + Future setOnConsoleMessage( + void Function(JavaScriptConsoleMessage consoleMessage) onConsoleMessage, + ) { + _onConsoleMessageCallback = onConsoleMessage; + + final JavaScriptChannelParams channelParams = WebKitJavaScriptChannelParams( + name: 'fltConsoleMessage', + webKitProxy: _webKitParams.webKitProxy, + onMessageReceived: (JavaScriptMessage message) { + if (_onConsoleMessageCallback == null) { + return; + } + + final Map consoleLog = + jsonDecode(message.message) as Map; + + JavaScriptLogLevel level; + switch (consoleLog['level']) { + case 'error': + level = JavaScriptLogLevel.error; + break; + case 'warning': + level = JavaScriptLogLevel.warning; + break; + case 'debug': + level = JavaScriptLogLevel.debug; + break; + case 'info': + level = JavaScriptLogLevel.info; + break; + case 'log': + default: + level = JavaScriptLogLevel.log; + break; + } + + _onConsoleMessageCallback!( + JavaScriptConsoleMessage( + level: level, + message: consoleLog['message']! as String, + ), + ); + }); + + addJavaScriptChannel(channelParams); + return _injectConsoleOverride(); + } + + Future _injectConsoleOverride() { + const WKUserScript overrideScript = WKUserScript( + ''' +function log(type, args) { + var message = Object.values(args) + .map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v) : v.toString()) + .map(v => v.substring(0, 3000)) // Limit msg to 3000 chars + .join(", "); + + var log = { + level: type, + message: message + }; + + window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log)); +} + +let originalLog = console.log; +let originalInfo = console.info; +let originalWarn = console.warn; +let originalError = console.error; +let originalDebug = console.debug; + +console.log = function() { log("log", arguments); originalLog.apply(null, arguments) }; +console.info = function() { log("info", arguments); originalInfo.apply(null, arguments) }; +console.warn = function() { log("warning", arguments); originalWarn.apply(null, arguments) }; +console.error = function() { log("error", arguments); originalError.apply(null, arguments) }; +console.debug = function() { log("debug", arguments); originalDebug.apply(null, arguments) }; + +window.addEventListener("error", function(e) { + log("error", e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno); +}); + ''', + WKUserScriptInjectionTime.atDocumentStart, + isMainFrameOnly: true, + ); + + return _webView.configuration.userContentController + .addUserScript(overrideScript); + } + // WKWebView does not support removing a single user script, so all user // scripts and all message handlers are removed instead. And the JavaScript // channels that shouldn't be removed are re-registered. Note that this @@ -537,6 +638,9 @@ class WebKitWebViewController extends PlatformWebViewController { // Zoom is disabled with a WKUserScript, so this adds it back if it was // removed above. if (!_zoomEnabled) _disableZoom(), + // Console logs are forwarded with a WKUserScript, so this adds it back + // if a console callback was registered with [setConsoleLogCallback]. + if (_onConsoleMessageCallback != null) _injectConsoleOverride(), ]); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 6e80a3964971..9473d0d34201 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.7.2 +version: 3.8.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -28,3 +28,8 @@ dev_dependencies: sdk: flutter mockito: 5.4.1 pigeon: ^10.1.4 + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {webview_flutter_platform_interface: {path: ../../webview_flutter/webview_flutter_platform_interface}} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.mocks.dart index b88d686b205d..5d0eede3b0ec 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart index 3c4210e8419b..b0112709f2b3 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; import 'dart:math' as _i2; @@ -760,6 +762,16 @@ class MockWKWebViewConfiguration extends _i1.Mock returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); @override + _i5.Future setLimitsNavigationsToAppBoundDomains(bool? limit) => + (super.noSuchMethod( + Invocation.method( + #setLimitsNavigationsToAppBoundDomains, + [limit], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override _i5.Future setMediaTypesRequiringUserActionForPlayback( Set<_i4.WKAudiovisualMediaType>? types) => (super.noSuchMethod( diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart index a648fc65a4dc..60cb608302b2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_wkwebview/test/src/foundation/foundation_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart' as _i3; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart index 65fdc41b275f..515028b1d1e5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i4; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart index e92132bdeb24..e46a4ce989be 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index 2cd20e0f6889..a22e2f6a3243 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -28,6 +28,7 @@ import 'webkit_webview_controller_test.mocks.dart'; WKWebsiteDataStore, WKWebView, WKWebViewConfiguration, + WKScriptMessageHandler, ]) void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -98,6 +99,7 @@ void main() { requestMediaCapturePermission: requestMediaCapturePermission, ); }, + createScriptMessageHandler: WKScriptMessageHandler.detached, ), instanceManager: instanceManager, ); @@ -1163,6 +1165,125 @@ void main() { await controller.setInspectable(true); verify(mockWebView.setInspectable(true)); }); + + group('Console logging', () { + test('setConsoleLogCallback should inject the correct JavaScript', + () async { + final MockWKUserContentController mockUserContentController = + MockWKUserContentController(); + final WebKitWebViewController controller = createControllerWithMocks( + mockUserContentController: mockUserContentController, + ); + + await controller + .setOnConsoleMessage((JavaScriptConsoleMessage message) {}); + + final List capturedScripts = + verify(mockUserContentController.addUserScript(captureAny)) + .captured + .toList(); + final WKUserScript messageHandlerScript = + capturedScripts[0] as WKUserScript; + final WKUserScript overrideConsoleScript = + capturedScripts[1] as WKUserScript; + + expect(messageHandlerScript.isMainFrameOnly, isFalse); + expect(messageHandlerScript.injectionTime, + WKUserScriptInjectionTime.atDocumentStart); + expect(messageHandlerScript.source, + 'window.fltConsoleMessage = webkit.messageHandlers.fltConsoleMessage;'); + + expect(overrideConsoleScript.isMainFrameOnly, isTrue); + expect(overrideConsoleScript.injectionTime, + WKUserScriptInjectionTime.atDocumentStart); + expect(overrideConsoleScript.source, ''' +function log(type, args) { + var message = Object.values(args) + .map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v) : v.toString()) + .map(v => v.substring(0, 3000)) // Limit msg to 3000 chars + .join(", "); + + var log = { + level: type, + message: message + }; + + window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log)); +} + +let originalLog = console.log; +let originalInfo = console.info; +let originalWarn = console.warn; +let originalError = console.error; +let originalDebug = console.debug; + +console.log = function() { log("log", arguments); originalLog.apply(null, arguments) }; +console.info = function() { log("info", arguments); originalInfo.apply(null, arguments) }; +console.warn = function() { log("warning", arguments); originalWarn.apply(null, arguments) }; +console.error = function() { log("error", arguments); originalError.apply(null, arguments) }; +console.debug = function() { log("debug", arguments); originalDebug.apply(null, arguments) }; + +window.addEventListener("error", function(e) { + log("error", e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno); +}); + '''); + }); + + test('setConsoleLogCallback should parse levels correctly', () async { + final MockWKUserContentController mockUserContentController = + MockWKUserContentController(); + final WebKitWebViewController controller = createControllerWithMocks( + mockUserContentController: mockUserContentController, + ); + + final Map logs = + {}; + await controller.setOnConsoleMessage( + (JavaScriptConsoleMessage message) => + logs[message.level] = message.message); + + final List capturedParameters = verify( + mockUserContentController.addScriptMessageHandler( + captureAny, any)) + .captured + .toList(); + final WKScriptMessageHandler scriptMessageHandler = + capturedParameters[0] as WKScriptMessageHandler; + + scriptMessageHandler.didReceiveScriptMessage( + mockUserContentController, + const WKScriptMessage( + name: 'test', + body: '{"level": "debug", "message": "Debug message"}')); + scriptMessageHandler.didReceiveScriptMessage( + mockUserContentController, + const WKScriptMessage( + name: 'test', + body: '{"level": "error", "message": "Error message"}')); + scriptMessageHandler.didReceiveScriptMessage( + mockUserContentController, + const WKScriptMessage( + name: 'test', + body: '{"level": "info", "message": "Info message"}')); + scriptMessageHandler.didReceiveScriptMessage( + mockUserContentController, + const WKScriptMessage( + name: 'test', + body: '{"level": "log", "message": "Log message"}')); + scriptMessageHandler.didReceiveScriptMessage( + mockUserContentController, + const WKScriptMessage( + name: 'test', + body: '{"level": "warning", "message": "Warning message"}')); + + expect(logs.length, 5); + expect(logs[JavaScriptLogLevel.debug], 'Debug message'); + expect(logs[JavaScriptLogLevel.error], 'Error message'); + expect(logs[JavaScriptLogLevel.info], 'Info message'); + expect(logs[JavaScriptLogLevel.log], 'Log message'); + expect(logs[JavaScriptLogLevel.warning], 'Warning message'); + }); + }); }); group('WebKitJavaScriptChannelParams', () { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart index 84f1587f45a3..7624a8724d98 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_wkwebview/test/webkit_webview_controller_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i6; import 'dart:math' as _i3; @@ -119,6 +121,17 @@ class _FakeWKWebView_8 extends _i1.SmartFake implements _i5.WKWebView { ); } +class _FakeWKScriptMessageHandler_9 extends _i1.SmartFake + implements _i5.WKScriptMessageHandler { + _FakeWKScriptMessageHandler_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [NSUrl]. /// /// See the documentation for Mockito's code generation for more information. @@ -924,3 +937,75 @@ class MockWKWebViewConfiguration extends _i1.Mock returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); } + +/// A class which mocks [WKScriptMessageHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKScriptMessageHandler extends _i1.Mock + implements _i5.WKScriptMessageHandler { + MockWKScriptMessageHandler() { + _i1.throwOnMissingStub(this); + } + + @override + void Function( + _i5.WKUserContentController, + _i5.WKScriptMessage, + ) get didReceiveScriptMessage => (super.noSuchMethod( + Invocation.getter(#didReceiveScriptMessage), + returnValue: ( + _i5.WKUserContentController userContentController, + _i5.WKScriptMessage message, + ) {}, + ) as void Function( + _i5.WKUserContentController, + _i5.WKScriptMessage, + )); + @override + _i5.WKScriptMessageHandler copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKScriptMessageHandler_9( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i5.WKScriptMessageHandler); + @override + _i6.Future addObserver( + _i2.NSObject? observer, { + required String? keyPath, + required Set<_i2.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override + _i6.Future removeObserver( + _i2.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.mocks.dart index 95818c3cefae..042631982cf5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart index b171e28a3bfb..8d104c1ed79d 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart @@ -1,7 +1,9 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.1 from annotations // in webview_flutter_wkwebview/test/webkit_webview_widget_test.dart. // Do not manually edit this file. +// @dart=2.19 + // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3;