Skip to content

Commit 7eee49a

Browse files
authored
[webview_flutter_wkwebview] Adds WKWebView implementation to override console log (flutter#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 flutter#4541. Related issue: 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].*
1 parent d0411e4 commit 7eee49a

15 files changed

+444
-12
lines changed

packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.8.0
2+
3+
* Adds support to register a callback to receive JavaScript console messages. See `WebKitWebViewController.setOnConsoleMessage`.
4+
15
## 3.7.4
26

37
* Adds pub topics to package metadata.

packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,6 +1196,48 @@ Future<void> main() async {
11961196
await expectLater(controller.currentUrl(), completion(primaryUrl));
11971197
},
11981198
);
1199+
1200+
group('Logging', () {
1201+
testWidgets('can receive console log messages',
1202+
(WidgetTester tester) async {
1203+
const String testPage = '''
1204+
<!DOCTYPE html>
1205+
<html>
1206+
<head>
1207+
<title>WebResourceError test</title>
1208+
</head>
1209+
<body onload="console.debug('Debug message')">
1210+
<p>Test page</p>
1211+
</body>
1212+
</html>
1213+
''';
1214+
1215+
final Completer<String> debugMessageReceived = Completer<String>();
1216+
final PlatformWebViewController controller = PlatformWebViewController(
1217+
const PlatformWebViewControllerCreationParams(),
1218+
);
1219+
unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
1220+
1221+
await controller
1222+
.setOnConsoleMessage((JavaScriptConsoleMessage consoleMessage) {
1223+
debugMessageReceived
1224+
.complete('${consoleMessage.level.name}:${consoleMessage.message}');
1225+
});
1226+
1227+
await controller.loadHtmlString(testPage);
1228+
1229+
await tester.pumpWidget(Builder(
1230+
builder: (BuildContext context) {
1231+
return PlatformWebViewWidget(
1232+
PlatformWebViewWidgetCreationParams(controller: controller),
1233+
).build(context);
1234+
},
1235+
));
1236+
1237+
await expectLater(
1238+
debugMessageReceived.future, completion('debug:Debug message'));
1239+
});
1240+
});
11991241
}
12001242

12011243
/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests.

packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,40 @@ const String kTransparentBackgroundPage = '''
7474
</html>
7575
''';
7676

77+
const String kLogExamplePage = '''
78+
<!DOCTYPE html>
79+
<html lang="en">
80+
<head>
81+
<title>Load file or HTML string example</title>
82+
</head>
83+
<body onload="console.log('Logging that the page is loading.')">
84+
85+
<h1>Local demo page</h1>
86+
<p>
87+
This page is used to test the forwarding of console logs to Dart.
88+
</p>
89+
90+
<style>
91+
.btn-group button {
92+
padding: 24px; 24px;
93+
display: block;
94+
width: 25%;
95+
margin: 5px 0px 0px 0px;
96+
}
97+
</style>
98+
99+
<div class="btn-group">
100+
<button onclick="console.error('This is an error message.')">Error</button>
101+
<button onclick="console.warn('This is a warning message.')">Warning</button>
102+
<button onclick="console.info('This is a info message.')">Info</button>
103+
<button onclick="console.debug('This is a debug message.')">Debug</button>
104+
<button onclick="console.log('This is a log message.')">Log</button>
105+
</div>
106+
107+
</body>
108+
</html>
109+
''';
110+
77111
class WebViewExample extends StatefulWidget {
78112
const WebViewExample({super.key, this.cookieManager});
79113

@@ -202,6 +236,7 @@ enum MenuOptions {
202236
loadHtmlString,
203237
transparentBackground,
204238
setCookie,
239+
logExample,
205240
}
206241

207242
class SampleMenu extends StatelessWidget {
@@ -262,6 +297,9 @@ class SampleMenu extends StatelessWidget {
262297
case MenuOptions.setCookie:
263298
_onSetCookie();
264299
break;
300+
case MenuOptions.logExample:
301+
_onLogExample();
302+
break;
265303
}
266304
},
267305
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
@@ -318,6 +356,10 @@ class SampleMenu extends StatelessWidget {
318356
value: MenuOptions.transparentBackground,
319357
child: Text('Transparent background example'),
320358
),
359+
const PopupMenuItem<MenuOptions>(
360+
value: MenuOptions.logExample,
361+
child: Text('Log example'),
362+
),
321363
],
322364
);
323365
}
@@ -466,6 +508,16 @@ class SampleMenu extends StatelessWidget {
466508

467509
return indexFile.path;
468510
}
511+
512+
Future<void> _onLogExample() {
513+
webViewController
514+
.setOnConsoleMessage((JavaScriptConsoleMessage consoleMessage) {
515+
debugPrint(
516+
'== JS == ${consoleMessage.level.name}: ${consoleMessage.message}');
517+
});
518+
519+
return webViewController.loadHtmlString(kLogExamplePage);
520+
}
469521
}
470522

471523
class NavigationControls extends StatelessWidget {

packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ dependencies:
1010
flutter:
1111
sdk: flutter
1212
path_provider: ^2.0.6
13-
webview_flutter_platform_interface: ^2.4.0
13+
webview_flutter_platform_interface: ^2.6.0
1414
webview_flutter_wkwebview:
1515
# When depending on this package from a real application you should use:
1616
# webview_flutter: ^x.y.z

packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67
import 'dart:math';
78

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

273+
void Function(JavaScriptConsoleMessage)? _onConsoleMessageCallback;
272274
void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback;
273275

274276
WebKitWebViewControllerCreationParams get _webKitParams =>
@@ -327,7 +329,8 @@ class WebKitWebViewController extends PlatformWebViewController {
327329
javaScriptChannelParams is WebKitJavaScriptChannelParams
328330
? javaScriptChannelParams
329331
: WebKitJavaScriptChannelParams.fromJavaScriptChannelParams(
330-
javaScriptChannelParams);
332+
javaScriptChannelParams,
333+
);
331334

332335
_javaScriptChannelParams[webKitParams.name] = webKitParams;
333336

@@ -512,6 +515,104 @@ class WebKitWebViewController extends PlatformWebViewController {
512515
.addUserScript(userScript);
513516
}
514517

518+
/// Sets a callback that notifies the host application of any log messages
519+
/// written to the JavaScript console.
520+
///
521+
/// Because the iOS WKWebView doesn't provide a built-in way to access the
522+
/// console, setting this callback will inject a custom [WKUserScript] which
523+
/// overrides the JavaScript `console.debug`, `console.error`, `console.info`,
524+
/// `console.log` and `console.warn` methods and forwards the console message
525+
/// via a `JavaScriptChannel` to the host application.
526+
@override
527+
Future<void> setOnConsoleMessage(
528+
void Function(JavaScriptConsoleMessage consoleMessage) onConsoleMessage,
529+
) {
530+
_onConsoleMessageCallback = onConsoleMessage;
531+
532+
final JavaScriptChannelParams channelParams = WebKitJavaScriptChannelParams(
533+
name: 'fltConsoleMessage',
534+
webKitProxy: _webKitParams.webKitProxy,
535+
onMessageReceived: (JavaScriptMessage message) {
536+
if (_onConsoleMessageCallback == null) {
537+
return;
538+
}
539+
540+
final Map<String, dynamic> consoleLog =
541+
jsonDecode(message.message) as Map<String, dynamic>;
542+
543+
JavaScriptLogLevel level;
544+
switch (consoleLog['level']) {
545+
case 'error':
546+
level = JavaScriptLogLevel.error;
547+
break;
548+
case 'warning':
549+
level = JavaScriptLogLevel.warning;
550+
break;
551+
case 'debug':
552+
level = JavaScriptLogLevel.debug;
553+
break;
554+
case 'info':
555+
level = JavaScriptLogLevel.info;
556+
break;
557+
case 'log':
558+
default:
559+
level = JavaScriptLogLevel.log;
560+
break;
561+
}
562+
563+
_onConsoleMessageCallback!(
564+
JavaScriptConsoleMessage(
565+
level: level,
566+
message: consoleLog['message']! as String,
567+
),
568+
);
569+
});
570+
571+
addJavaScriptChannel(channelParams);
572+
return _injectConsoleOverride();
573+
}
574+
575+
Future<void> _injectConsoleOverride() {
576+
const WKUserScript overrideScript = WKUserScript(
577+
'''
578+
function log(type, args) {
579+
var message = Object.values(args)
580+
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v) : v.toString())
581+
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
582+
.join(", ");
583+
584+
var log = {
585+
level: type,
586+
message: message
587+
};
588+
589+
window.webkit.messageHandlers.fltConsoleMessage.postMessage(JSON.stringify(log));
590+
}
591+
592+
let originalLog = console.log;
593+
let originalInfo = console.info;
594+
let originalWarn = console.warn;
595+
let originalError = console.error;
596+
let originalDebug = console.debug;
597+
598+
console.log = function() { log("log", arguments); originalLog.apply(null, arguments) };
599+
console.info = function() { log("info", arguments); originalInfo.apply(null, arguments) };
600+
console.warn = function() { log("warning", arguments); originalWarn.apply(null, arguments) };
601+
console.error = function() { log("error", arguments); originalError.apply(null, arguments) };
602+
console.debug = function() { log("debug", arguments); originalDebug.apply(null, arguments) };
603+
604+
window.addEventListener("error", function(e) {
605+
log("error", e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno);
606+
});
607+
''',
608+
WKUserScriptInjectionTime.atDocumentStart,
609+
isMainFrameOnly: true,
610+
);
611+
612+
return _webView.configuration.userContentController
613+
.addUserScript(overrideScript);
614+
}
615+
515616
// WKWebView does not support removing a single user script, so all user
516617
// scripts and all message handlers are removed instead. And the JavaScript
517618
// channels that shouldn't be removed are re-registered. Note that this
@@ -537,6 +638,9 @@ class WebKitWebViewController extends PlatformWebViewController {
537638
// Zoom is disabled with a WKUserScript, so this adds it back if it was
538639
// removed above.
539640
if (!_zoomEnabled) _disableZoom(),
641+
// Console logs are forwarded with a WKUserScript, so this adds it back
642+
// if a console callback was registered with [setOnConsoleMessage].
643+
if (_onConsoleMessageCallback != null) _injectConsoleOverride(),
540644
]);
541645
}
542646

packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: webview_flutter_wkwebview
22
description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control.
33
repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
5-
version: 3.7.4
5+
version: 3.8.0
66

77
environment:
88
sdk: ">=2.19.0 <4.0.0"
@@ -20,7 +20,7 @@ dependencies:
2020
flutter:
2121
sdk: flutter
2222
path: ^1.8.0
23-
webview_flutter_platform_interface: ^2.4.0
23+
webview_flutter_platform_interface: ^2.6.0
2424

2525
dev_dependencies:
2626
build_runner: ^2.1.5

packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.mocks.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
// Mocks generated by Mockito 5.4.0 from annotations
1+
// Mocks generated by Mockito 5.4.1 from annotations
22
// in webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.dart.
33
// Do not manually edit this file.
44

5+
// @dart=2.19
6+
57
// ignore_for_file: no_leading_underscores_for_library_prefixes
68
import 'dart:async' as _i3;
79

packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
// Mocks generated by Mockito 5.4.0 from annotations
1+
// Mocks generated by Mockito 5.4.1 from annotations
22
// in webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.dart.
33
// Do not manually edit this file.
44

5+
// @dart=2.19
6+
57
// ignore_for_file: no_leading_underscores_for_library_prefixes
68
import 'dart:async' as _i5;
79
import 'dart:math' as _i2;
@@ -760,6 +762,16 @@ class MockWKWebViewConfiguration extends _i1.Mock
760762
returnValueForMissingStub: _i5.Future<void>.value(),
761763
) as _i5.Future<void>);
762764
@override
765+
_i5.Future<void> setLimitsNavigationsToAppBoundDomains(bool? limit) =>
766+
(super.noSuchMethod(
767+
Invocation.method(
768+
#setLimitsNavigationsToAppBoundDomains,
769+
[limit],
770+
),
771+
returnValue: _i5.Future<void>.value(),
772+
returnValueForMissingStub: _i5.Future<void>.value(),
773+
) as _i5.Future<void>);
774+
@override
763775
_i5.Future<void> setMediaTypesRequiringUserActionForPlayback(
764776
Set<_i4.WKAudiovisualMediaType>? types) =>
765777
(super.noSuchMethod(

packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
// Mocks generated by Mockito 5.4.0 from annotations
1+
// Mocks generated by Mockito 5.4.1 from annotations
22
// in webview_flutter_wkwebview/test/src/foundation/foundation_test.dart.
33
// Do not manually edit this file.
44

5+
// @dart=2.19
6+
57
// ignore_for_file: no_leading_underscores_for_library_prefixes
68
import 'package:mockito/mockito.dart' as _i1;
79
import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart' as _i3;

packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
// Mocks generated by Mockito 5.4.0 from annotations
1+
// Mocks generated by Mockito 5.4.1 from annotations
22
// in webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart.
33
// Do not manually edit this file.
44

5+
// @dart=2.19
6+
57
// ignore_for_file: no_leading_underscores_for_library_prefixes
68
import 'dart:async' as _i4;
79

0 commit comments

Comments
 (0)