Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[webview_flutter_wkwebview] Adds WKWebView implementation to override console log #4703

Merged
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.8.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this was overridden or this PR needs to pull in main.


* Adds support to register a callback to receive JavaScript console messages. See `WebKitWebViewController.setOnConsoleMessage`.

## 3.7.4

* Adds pub topics to package metadata.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,48 @@ Future<void> main() async {
await expectLater(controller.currentUrl(), completion(primaryUrl));
},
);

group('Logging', () {
testWidgets('can receive console log messages',
(WidgetTester tester) async {
const String testPage = '''
<!DOCTYPE html>
<html>
<head>
<title>WebResourceError test</title>
</head>
<body onload="console.debug('Debug message')">
<p>Test page</p>
</body>
</html>
''';

final Completer<String> debugMessageReceived = Completer<String>();
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,40 @@ const String kTransparentBackgroundPage = '''
</html>
''';

const String kLogExamplePage = '''
<!DOCTYPE html>
<html lang="en">
<head>
<title>Load file or HTML string example</title>
</head>
<body onload="console.log('Logging that the page is loading.')">

<h1>Local demo page</h1>
<p>
This page is used to test the forwarding of console logs to Dart.
</p>

<style>
.btn-group button {
padding: 24px; 24px;
display: block;
width: 25%;
margin: 5px 0px 0px 0px;
}
</style>

<div class="btn-group">
<button onclick="console.error('This is an error message.')">Error</button>
<button onclick="console.warn('This is a warning message.')">Warning</button>
<button onclick="console.info('This is a info message.')">Info</button>
<button onclick="console.debug('This is a debug message.')">Debug</button>
<button onclick="console.log('This is a log message.')">Log</button>
</div>

</body>
</html>
''';

class WebViewExample extends StatefulWidget {
const WebViewExample({super.key, this.cookieManager});

Expand Down Expand Up @@ -202,6 +236,7 @@ enum MenuOptions {
loadHtmlString,
transparentBackground,
setCookie,
logExample,
}

class SampleMenu extends StatelessWidget {
Expand Down Expand Up @@ -262,6 +297,9 @@ class SampleMenu extends StatelessWidget {
case MenuOptions.setCookie:
_onSetCookie();
break;
case MenuOptions.logExample:
_onLogExample();
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
Expand Down Expand Up @@ -318,6 +356,10 @@ class SampleMenu extends StatelessWidget {
value: MenuOptions.transparentBackground,
child: Text('Transparent background example'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.logExample,
child: Text('Log example'),
),
],
);
}
Expand Down Expand Up @@ -466,6 +508,16 @@ class SampleMenu extends StatelessWidget {

return indexFile.path;
}

Future<void> _onLogExample() {
webViewController
.setOnConsoleMessage((JavaScriptConsoleMessage consoleMessage) {
debugPrint(
'== JS == ${consoleMessage.level.name}: ${consoleMessage.message}');
});

return webViewController.loadHtmlString(kLogExamplePage);
}
}

class NavigationControls extends StatelessWidget {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies:
flutter:
sdk: flutter
path_provider: ^2.0.6
webview_flutter_platform_interface: ^2.4.0
webview_flutter_platform_interface: ^2.6.0
webview_flutter_wkwebview:
# When depending on this package from a real application you should use:
# webview_flutter: ^x.y.z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:math';

import 'package:flutter/material.dart';
Expand Down Expand Up @@ -269,6 +270,7 @@ class WebKitWebViewController extends PlatformWebViewController {
bool _zoomEnabled = true;
WebKitNavigationDelegate? _currentNavigationDelegate;

void Function(JavaScriptConsoleMessage)? _onConsoleMessageCallback;
void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback;

WebKitWebViewControllerCreationParams get _webKitParams =>
Expand Down Expand Up @@ -327,7 +329,8 @@ class WebKitWebViewController extends PlatformWebViewController {
javaScriptChannelParams is WebKitJavaScriptChannelParams
? javaScriptChannelParams
: WebKitJavaScriptChannelParams.fromJavaScriptChannelParams(
javaScriptChannelParams);
javaScriptChannelParams,
);

_javaScriptChannelParams[webKitParams.name] = webKitParams;

Expand Down Expand Up @@ -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<void> 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<String, dynamic> consoleLog =
jsonDecode(message.message) as Map<String, dynamic>;

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<void> _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
Expand All @@ -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 [setOnConsoleMessage].
if (_onConsoleMessageCallback != null) _injectConsoleOverride(),
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.4
version: 3.8.0

environment:
sdk: ">=2.19.0 <4.0.0"
Expand All @@ -20,7 +20,7 @@ dependencies:
flutter:
sdk: flutter
path: ^1.8.0
webview_flutter_platform_interface: ^2.4.0
webview_flutter_platform_interface: ^2.6.0

dev_dependencies:
build_runner: ^2.1.5
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -760,6 +762,16 @@ class MockWKWebViewConfiguration extends _i1.Mock
returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>);
@override
_i5.Future<void> setLimitsNavigationsToAppBoundDomains(bool? limit) =>
(super.noSuchMethod(
Invocation.method(
#setLimitsNavigationsToAppBoundDomains,
[limit],
),
returnValue: _i5.Future<void>.value(),
returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>);
@override
_i5.Future<void> setMediaTypesRequiringUserActionForPlayback(
Set<_i4.WKAudiovisualMediaType>? types) =>
(super.noSuchMethod(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Loading