Skip to content

Commit

Permalink
[webview_flutter_wkwebview] Adds WKWebView implementation to override…
Browse files Browse the repository at this point in the history
… console log (#4703)

Adds the WKWebView implementation for registering a JavaScript console callback. This will allow developers to receive JavaScript console messages in a Dart callback.

This PR contains the `webview_flutter_wkwebview` specific changes from PR #4541.

Related issue: flutter/flutter#32908

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
  • Loading branch information
mvanbeusekom authored Sep 21, 2023
1 parent d0411e4 commit 7eee49a
Show file tree
Hide file tree
Showing 15 changed files with 444 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.8.0

* 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

0 comments on commit 7eee49a

Please sign in to comment.