[webview_flutter_wkwebview] Implementation of WebKitWebViewController for WebKit (#6105)

diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_proxy.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_proxy.dart
new file mode 100644
index 0000000..48e6faf
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_proxy.dart
@@ -0,0 +1,47 @@
+// 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 '../../foundation/foundation.dart';
+import '../../web_kit/web_kit.dart';
+
+/// Handles constructing objects and calling static methods for the WebKit
+/// native library.
+///
+/// This class provides dependency injection for the implementations of the
+/// platform interface classes. Improving the ease of unit testing and/or
+/// overriding the underlying WebKit classes.
+///
+/// By default each function calls the default constructor of the WebKit class
+/// it intends to return.
+class WebKitProxy {
+  /// Constructs a [WebKitProxy].
+  const WebKitProxy({
+    this.createWebView = WKWebView.new,
+    this.createWebViewConfiguration = WKWebViewConfiguration.new,
+    this.createScriptMessageHandler = WKScriptMessageHandler.new,
+  });
+
+  /// Constructs a [WKWebView].
+  final WKWebView Function(
+    WKWebViewConfiguration configuration, {
+    void Function(
+      String keyPath,
+      NSObject object,
+      Map<NSKeyValueChangeKey, Object?> change,
+    )
+        observeValue,
+  }) createWebView;
+
+  /// Constructs a [WKWebViewConfiguration].
+  final WKWebViewConfiguration Function() createWebViewConfiguration;
+
+  /// Constructs a [WKScriptMessageHandler].
+  final WKScriptMessageHandler Function({
+    required void Function(
+      WKUserContentController userContentController,
+      WKScriptMessage message,
+    )
+        didReceiveScriptMessage,
+  }) createScriptMessageHandler;
+}
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_controller.dart
new file mode 100644
index 0000000..9d76b5c
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_controller.dart
@@ -0,0 +1,354 @@
+// 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 'dart:math';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:path/path.dart' as path;
+import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart';
+
+import '../../common/weak_reference_utils.dart';
+import '../../foundation/foundation.dart';
+import '../../web_kit/web_kit.dart';
+import 'webkit_proxy.dart';
+
+/// Object specifying creation parameters for a [WebKitWebViewController].
+@immutable
+class WebKitWebViewControllerCreationParams
+    extends PlatformWebViewControllerCreationParams {
+  /// Constructs a [WebKitWebViewControllerCreationParams].
+  WebKitWebViewControllerCreationParams({
+    @visibleForTesting WebKitProxy webKitProxy = const WebKitProxy(),
+  }) : _configuration = webKitProxy.createWebViewConfiguration();
+
+  /// Constructs a [WebKitWebViewControllerCreationParams] using a
+  /// [PlatformWebViewControllerCreationParams].
+  WebKitWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams(
+    // Recommended placeholder to prevent being broken by platform interface.
+    // ignore: avoid_unused_constructor_parameters
+    PlatformWebViewControllerCreationParams params, {
+    @visibleForTesting WebKitProxy webKitProxy = const WebKitProxy(),
+  }) : this(webKitProxy: webKitProxy);
+
+  final WKWebViewConfiguration _configuration;
+}
+
+/// An implementation of [PlatformWebViewController] with the WebKit api.
+class WebKitWebViewController extends PlatformWebViewController {
+  /// Constructs a [WebKitWebViewController].
+  WebKitWebViewController(
+    PlatformWebViewControllerCreationParams params, {
+    @visibleForTesting WebKitProxy webKitProxy = const WebKitProxy(),
+  }) : super.implementation(params is WebKitWebViewControllerCreationParams
+            ? params
+            : WebKitWebViewControllerCreationParams
+                .fromPlatformWebViewControllerCreationParams(params)) {
+    _webView = webKitProxy.createWebView(
+        (params as WebKitWebViewControllerCreationParams)._configuration);
+  }
+
+  late final WKWebView _webView;
+
+  final Map<String, WebKitJavaScriptChannelParams> _javaScriptChannelParams =
+      <String, WebKitJavaScriptChannelParams>{};
+
+  bool _zoomEnabled = true;
+
+  @override
+  Future<void> loadFile(String absoluteFilePath) {
+    return _webView.loadFileUrl(
+      absoluteFilePath,
+      readAccessUrl: path.dirname(absoluteFilePath),
+    );
+  }
+
+  @override
+  Future<void> loadFlutterAsset(String key) {
+    assert(key.isNotEmpty);
+    return _webView.loadFlutterAsset(key);
+  }
+
+  @override
+  Future<void> loadHtmlString(String html, {String? baseUrl}) {
+    return _webView.loadHtmlString(html, baseUrl: baseUrl);
+  }
+
+  @override
+  Future<void> loadRequest(LoadRequestParams params) {
+    if (!params.uri.hasScheme) {
+      throw ArgumentError(
+        'LoadRequestParams#uri is required to have a scheme.',
+      );
+    }
+
+    return _webView.loadRequest(NSUrlRequest(
+      url: params.uri.toString(),
+      allHttpHeaderFields: params.headers,
+      httpMethod: describeEnum(params.method),
+      httpBody: params.body,
+    ));
+  }
+
+  @override
+  Future<void> addJavaScriptChannel(
+    JavaScriptChannelParams javaScriptChannelParams,
+  ) {
+    final WebKitJavaScriptChannelParams webKitParams =
+        javaScriptChannelParams is WebKitJavaScriptChannelParams
+            ? javaScriptChannelParams
+            : WebKitJavaScriptChannelParams.fromJavaScriptChannelParams(
+                javaScriptChannelParams);
+
+    _javaScriptChannelParams[webKitParams.name] = webKitParams;
+
+    final String wrapperSource =
+        'window.${webKitParams.name} = webkit.messageHandlers.${webKitParams.name};';
+    final WKUserScript wrapperScript = WKUserScript(
+      wrapperSource,
+      WKUserScriptInjectionTime.atDocumentStart,
+      isMainFrameOnly: false,
+    );
+    _webView.configuration.userContentController.addUserScript(wrapperScript);
+    return _webView.configuration.userContentController.addScriptMessageHandler(
+      webKitParams._messageHandler,
+      webKitParams.name,
+    );
+  }
+
+  @override
+  Future<void> removeJavaScriptChannel(String javaScriptChannelName) async {
+    assert(javaScriptChannelName.isNotEmpty);
+    if (!_javaScriptChannelParams.containsKey(javaScriptChannelName)) {
+      return;
+    }
+    await _resetUserScripts(removedJavaScriptChannel: javaScriptChannelName);
+  }
+
+  @override
+  Future<String?> currentUrl() => _webView.getUrl();
+
+  @override
+  Future<bool> canGoBack() => _webView.canGoBack();
+
+  @override
+  Future<bool> canGoForward() => _webView.canGoForward();
+
+  @override
+  Future<void> goBack() => _webView.goBack();
+
+  @override
+  Future<void> goForward() => _webView.goForward();
+
+  @override
+  Future<void> reload() => _webView.reload();
+
+  @override
+  Future<void> clearCache() {
+    return _webView.configuration.websiteDataStore.removeDataOfTypes(
+      <WKWebsiteDataType>{
+        WKWebsiteDataType.memoryCache,
+        WKWebsiteDataType.diskCache,
+        WKWebsiteDataType.offlineWebApplicationCache,
+      },
+      DateTime.fromMillisecondsSinceEpoch(0),
+    );
+  }
+
+  @override
+  Future<void> clearLocalStorage() {
+    return _webView.configuration.websiteDataStore.removeDataOfTypes(
+      <WKWebsiteDataType>{WKWebsiteDataType.localStorage},
+      DateTime.fromMillisecondsSinceEpoch(0),
+    );
+  }
+
+  @override
+  Future<void> runJavaScript(String javaScript) async {
+    try {
+      await _webView.evaluateJavaScript(javaScript);
+    } on PlatformException catch (exception) {
+      // WebKit will throw an error when the type of the evaluated value is
+      // unsupported. This also goes for `null` and `undefined` on iOS 14+. For
+      // example, when running a void function. For ease of use, this specific
+      // error is ignored when no return value is expected.
+      if (exception.details is! NSError ||
+          exception.details.code !=
+              WKErrorCode.javaScriptResultTypeIsUnsupported) {
+        rethrow;
+      }
+    }
+  }
+
+  @override
+  Future<String> runJavaScriptReturningResult(String javaScript) async {
+    final Object? result = await _webView.evaluateJavaScript(javaScript);
+    if (result == null) {
+      throw ArgumentError(
+        'Result of JavaScript execution returned a `null` value. '
+        'Use `runJavascript` when expecting a null return value.',
+      );
+    }
+    return result.toString();
+  }
+
+  /// Controls whether inline playback of HTML5 videos is allowed.
+  Future<void> setAllowsInlineMediaPlayback(bool allow) {
+    return _webView.configuration.setAllowsInlineMediaPlayback(allow);
+  }
+
+  @override
+  Future<String?> getTitle() => _webView.getTitle();
+
+  @override
+  Future<void> scrollTo(int x, int y) {
+    return _webView.scrollView.setContentOffset(Point<double>(
+      x.toDouble(),
+      y.toDouble(),
+    ));
+  }
+
+  @override
+  Future<void> scrollBy(int x, int y) {
+    return _webView.scrollView.scrollBy(Point<double>(
+      x.toDouble(),
+      y.toDouble(),
+    ));
+  }
+
+  @override
+  Future<Point<int>> getScrollPosition() async {
+    final Point<double> offset = await _webView.scrollView.getContentOffset();
+    return Point<int>(offset.x.round(), offset.y.round());
+  }
+
+  // TODO(bparrishMines): This is unique to iOS. Override should be removed if
+  // this is removed from the platform interface before webview_flutter version
+  // 4.0.0.
+  @override
+  Future<void> enableGestureNavigation(bool enabled) {
+    return _webView.setAllowsBackForwardNavigationGestures(enabled);
+  }
+
+  @override
+  Future<void> setBackgroundColor(Color color) {
+    return Future.wait(<Future<void>>[
+      _webView.scrollView.setBackgroundColor(color),
+      _webView.setOpaque(false),
+      _webView.setBackgroundColor(Colors.transparent),
+    ]);
+  }
+
+  @override
+  Future<void> setJavaScriptMode(JavaScriptMode javaScriptMode) {
+    switch (javaScriptMode) {
+      case JavaScriptMode.disabled:
+        return _webView.configuration.preferences.setJavaScriptEnabled(false);
+      case JavaScriptMode.unrestricted:
+        return _webView.configuration.preferences.setJavaScriptEnabled(true);
+    }
+  }
+
+  @override
+  Future<void> setUserAgent(String? userAgent) {
+    return _webView.setCustomUserAgent(userAgent);
+  }
+
+  @override
+  Future<void> enableZoom(bool enabled) async {
+    if (_zoomEnabled == enabled) {
+      return;
+    }
+
+    _zoomEnabled = enabled;
+    if (enabled) {
+      await _resetUserScripts();
+    } else {
+      await _disableZoom();
+    }
+  }
+
+  Future<void> _disableZoom() {
+    const WKUserScript userScript = WKUserScript(
+      "var meta = document.createElement('meta');\n"
+      "meta.name = 'viewport';\n"
+      "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, "
+      "user-scalable=no';\n"
+      "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);",
+      WKUserScriptInjectionTime.atDocumentEnd,
+      isMainFrameOnly: true,
+    );
+    return _webView.configuration.userContentController
+        .addUserScript(userScript);
+  }
+
+  // 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
+  // workaround could interfere with exposing support for custom scripts from
+  // applications.
+  Future<void> _resetUserScripts({String? removedJavaScriptChannel}) async {
+    _webView.configuration.userContentController.removeAllUserScripts();
+    // TODO(bparrishMines): This can be replaced with
+    // `removeAllScriptMessageHandlers` once Dart supports runtime version
+    // checking. (e.g. The equivalent to @availability in Objective-C.)
+    _javaScriptChannelParams.keys.forEach(
+      _webView.configuration.userContentController.removeScriptMessageHandler,
+    );
+
+    _javaScriptChannelParams.remove(removedJavaScriptChannel);
+
+    await Future.wait(<Future<void>>[
+      for (JavaScriptChannelParams params in _javaScriptChannelParams.values)
+        addJavaScriptChannel(params),
+      // Zoom is disabled with a WKUserScript, so this adds it back if it was
+      // removed above.
+      if (!_zoomEnabled) _disableZoom(),
+    ]);
+  }
+}
+
+/// An implementation of [JavaScriptChannelParams] with the WebKit api.
+///
+/// See [WebKitWebViewController.addJavaScriptChannel].
+@immutable
+class WebKitJavaScriptChannelParams extends JavaScriptChannelParams {
+  /// Constructs a [WebKitJavaScriptChannelParams].
+  WebKitJavaScriptChannelParams({
+    required super.name,
+    required super.onMessageReceived,
+    @visibleForTesting WebKitProxy webKitProxy = const WebKitProxy(),
+  })  : assert(name.isNotEmpty),
+        _messageHandler = webKitProxy.createScriptMessageHandler(
+          didReceiveScriptMessage: withWeakRefenceTo(
+            onMessageReceived,
+            (WeakReference<void Function(JavaScriptMessage)> weakReference) {
+              return (
+                WKUserContentController controller,
+                WKScriptMessage message,
+              ) {
+                if (weakReference.target != null) {
+                  weakReference.target!(
+                    JavaScriptMessage(message: message.body!.toString()),
+                  );
+                }
+              };
+            },
+          ),
+        );
+
+  /// Constructs a [WebKitJavaScriptChannelParams] using a
+  /// [JavaScriptChannelParams].
+  WebKitJavaScriptChannelParams.fromJavaScriptChannelParams(
+    JavaScriptChannelParams params, {
+    @visibleForTesting WebKitProxy webKitProxy = const WebKitProxy(),
+  }) : this(
+          name: params.name,
+          onMessageReceived: params.onMessageReceived,
+          webKitProxy: webKitProxy,
+        );
+
+  final WKScriptMessageHandler _messageHandler;
+}
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_platform.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_platform.dart
new file mode 100644
index 0000000..b808086
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_platform.dart
@@ -0,0 +1,17 @@
+// 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:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart';
+
+import 'webkit_webview_controller.dart';
+
+/// Implementation of [WebViewPlatform] using the WebKit Api.
+class WebKitWebViewPlatform extends WebViewPlatform {
+  @override
+  WebKitWebViewController createPlatformWebViewController(
+    PlatformWebViewControllerCreationParams params,
+  ) {
+    return WebKitWebViewController(params);
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/webview_flutter_wkwebview.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/webview_flutter_wkwebview.dart
new file mode 100644
index 0000000..2a593fb
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/webview_flutter_wkwebview.dart
@@ -0,0 +1,7 @@
+// 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.
+
+library webview_flutter_wkwebview;
+
+export 'src/webkit_webview_controller.dart';
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart
new file mode 100644
index 0000000..cf5da17
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart
@@ -0,0 +1,746 @@
+// 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 'dart:math';
+// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231)
+// ignore: unnecessary_import
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart';
+import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart';
+import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart';
+import 'package:webview_flutter_wkwebview/src/v4/src/webkit_proxy.dart';
+import 'package:webview_flutter_wkwebview/src/v4/webview_flutter_wkwebview.dart';
+import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart';
+
+import 'webkit_webview_controller_test.mocks.dart';
+
+@GenerateMocks(<Type>[
+  UIScrollView,
+  WKPreferences,
+  WKUserContentController,
+  WKWebsiteDataStore,
+  WKWebView,
+  WKWebViewConfiguration,
+])
+void main() {
+  WidgetsFlutterBinding.ensureInitialized();
+
+  group('WebKitWebViewController', () {
+    WebKitWebViewController createControllerWithMocks({
+      MockUIScrollView? mockScrollView,
+      MockWKPreferences? mockPreferences,
+      MockWKUserContentController? mockUserContentController,
+      MockWKWebsiteDataStore? mockWebsiteDataStore,
+      MockWKWebView Function(
+        WKWebViewConfiguration configuration, {
+        void Function(
+          String keyPath,
+          NSObject object,
+          Map<NSKeyValueChangeKey, Object?> change,
+        )?
+            observeValue,
+      })?
+          createMockWebView,
+      MockWKWebViewConfiguration? mockWebViewConfiguration,
+    }) {
+      final MockWKWebViewConfiguration nonNullMockWebViewConfiguration =
+          mockWebViewConfiguration ?? MockWKWebViewConfiguration();
+      late final MockWKWebView nonNullMockWebView;
+
+      final PlatformWebViewControllerCreationParams controllerCreationParams =
+          WebKitWebViewControllerCreationParams(
+        webKitProxy: WebKitProxy(
+          createWebViewConfiguration: () => nonNullMockWebViewConfiguration,
+        ),
+      );
+
+      final WebKitWebViewController controller = WebKitWebViewController(
+        controllerCreationParams,
+        webKitProxy: WebKitProxy(
+          createWebView: (
+            _, {
+            void Function(
+              String keyPath,
+              NSObject object,
+              Map<NSKeyValueChangeKey, Object?> change,
+            )?
+                observeValue,
+          }) {
+            nonNullMockWebView = createMockWebView == null
+                ? MockWKWebView()
+                : createMockWebView(
+                    nonNullMockWebViewConfiguration,
+                    observeValue: observeValue,
+                  );
+            return nonNullMockWebView;
+          },
+        ),
+      );
+
+      when(nonNullMockWebView.scrollView)
+          .thenReturn(mockScrollView ?? MockUIScrollView());
+      when(nonNullMockWebView.configuration)
+          .thenReturn(nonNullMockWebViewConfiguration);
+
+      when(nonNullMockWebViewConfiguration.preferences)
+          .thenReturn(mockPreferences ?? MockWKPreferences());
+      when(nonNullMockWebViewConfiguration.userContentController).thenReturn(
+          mockUserContentController ?? MockWKUserContentController());
+      when(nonNullMockWebViewConfiguration.websiteDataStore)
+          .thenReturn(mockWebsiteDataStore ?? MockWKWebsiteDataStore());
+
+      return controller;
+    }
+
+    test('loadFile', () async {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      await controller.loadFile('/path/to/file.html');
+      verify(mockWebView.loadFileUrl(
+        '/path/to/file.html',
+        readAccessUrl: '/path/to',
+      ));
+    });
+
+    test('loadFlutterAsset', () async {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      await controller.loadFlutterAsset('test_assets/index.html');
+      verify(mockWebView.loadFlutterAsset('test_assets/index.html'));
+    });
+
+    test('loadHtmlString', () async {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      const String htmlString = '<html><body>Test data.</body></html>';
+      await controller.loadHtmlString(htmlString, baseUrl: 'baseUrl');
+
+      verify(mockWebView.loadHtmlString(
+        '<html><body>Test data.</body></html>',
+        baseUrl: 'baseUrl',
+      ));
+    });
+
+    group('loadRequest', () {
+      test('Throws ArgumentError for empty scheme', () async {
+        final MockWKWebView mockWebView = MockWKWebView();
+
+        final WebKitWebViewController controller = createControllerWithMocks(
+          createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+        );
+
+        expect(
+          () async => await controller.loadRequest(
+            LoadRequestParams(
+              uri: Uri.parse('www.google.com'),
+              method: LoadRequestMethod.get,
+              headers: const <String, String>{},
+            ),
+          ),
+          throwsA(isA<ArgumentError>()),
+        );
+      });
+
+      test('GET without headers', () async {
+        final MockWKWebView mockWebView = MockWKWebView();
+
+        final WebKitWebViewController controller = createControllerWithMocks(
+          createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+        );
+
+        await controller.loadRequest(
+          LoadRequestParams(
+            uri: Uri.parse('https://www.google.com'),
+            method: LoadRequestMethod.get,
+            headers: const <String, String>{},
+          ),
+        );
+
+        final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny))
+            .captured
+            .single as NSUrlRequest;
+        expect(request.url, 'https://www.google.com');
+        expect(request.allHttpHeaderFields, <String, String>{});
+        expect(request.httpMethod, 'get');
+      });
+
+      test('GET with headers', () async {
+        final MockWKWebView mockWebView = MockWKWebView();
+
+        final WebKitWebViewController controller = createControllerWithMocks(
+          createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+        );
+
+        await controller.loadRequest(
+          LoadRequestParams(
+            uri: Uri.parse('https://www.google.com'),
+            method: LoadRequestMethod.get,
+            headers: const <String, String>{'a': 'header'},
+          ),
+        );
+
+        final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny))
+            .captured
+            .single as NSUrlRequest;
+        expect(request.url, 'https://www.google.com');
+        expect(request.allHttpHeaderFields, <String, String>{'a': 'header'});
+        expect(request.httpMethod, 'get');
+      });
+
+      test('POST without body', () async {
+        final MockWKWebView mockWebView = MockWKWebView();
+
+        final WebKitWebViewController controller = createControllerWithMocks(
+          createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+        );
+
+        await controller.loadRequest(LoadRequestParams(
+          uri: Uri.parse('https://www.google.com'),
+          method: LoadRequestMethod.post,
+          headers: const <String, String>{},
+        ));
+
+        final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny))
+            .captured
+            .single as NSUrlRequest;
+        expect(request.url, 'https://www.google.com');
+        expect(request.httpMethod, 'post');
+      });
+
+      test('POST with body', () async {
+        final MockWKWebView mockWebView = MockWKWebView();
+
+        final WebKitWebViewController controller = createControllerWithMocks(
+          createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+        );
+
+        await controller.loadRequest(LoadRequestParams(
+          uri: Uri.parse('https://www.google.com'),
+          method: LoadRequestMethod.post,
+          body: Uint8List.fromList('Test Body'.codeUnits),
+          headers: const <String, String>{},
+        ));
+
+        final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny))
+            .captured
+            .single as NSUrlRequest;
+        expect(request.url, 'https://www.google.com');
+        expect(request.httpMethod, 'post');
+        expect(
+          request.httpBody,
+          Uint8List.fromList('Test Body'.codeUnits),
+        );
+      });
+    });
+
+    test('canGoBack', () {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      when(mockWebView.canGoBack()).thenAnswer(
+        (_) => Future<bool>.value(false),
+      );
+      expect(controller.canGoBack(), completion(false));
+    });
+
+    test('canGoForward', () {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      when(mockWebView.canGoForward()).thenAnswer(
+        (_) => Future<bool>.value(true),
+      );
+      expect(controller.canGoForward(), completion(true));
+    });
+
+    test('goBack', () async {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      await controller.goBack();
+      verify(mockWebView.goBack());
+    });
+
+    test('goForward', () async {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      await controller.goForward();
+      verify(mockWebView.goForward());
+    });
+
+    test('reload', () async {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      await controller.reload();
+      verify(mockWebView.reload());
+    });
+
+    test('enableGestureNavigation', () async {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      await controller.enableGestureNavigation(true);
+      verify(mockWebView.setAllowsBackForwardNavigationGestures(true));
+    });
+
+    test('runJavaScriptReturningResult', () {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer(
+        (_) => Future<String>.value('returnString'),
+      );
+      expect(
+        controller.runJavaScriptReturningResult('runJavaScript'),
+        completion('returnString'),
+      );
+    });
+
+    test('runJavaScriptReturningResult throws error on null return value', () {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer(
+        (_) => Future<String?>.value(null),
+      );
+      expect(
+        () => controller.runJavaScriptReturningResult('runJavaScript'),
+        throwsArgumentError,
+      );
+    });
+
+    test('runJavaScript', () {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer(
+        (_) => Future<String>.value('returnString'),
+      );
+      expect(
+        controller.runJavaScript('runJavaScript'),
+        completes,
+      );
+    });
+
+    test('runJavaScript ignores exception with unsupported javaScript type',
+        () {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      when(mockWebView.evaluateJavaScript('runJavaScript'))
+          .thenThrow(PlatformException(
+        code: '',
+        details: const NSError(
+          code: WKErrorCode.javaScriptResultTypeIsUnsupported,
+          domain: '',
+          localizedDescription: '',
+        ),
+      ));
+      expect(
+        controller.runJavaScript('runJavaScript'),
+        completes,
+      );
+    });
+
+    test('getTitle', () async {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      when(mockWebView.getTitle())
+          .thenAnswer((_) => Future<String>.value('Web Title'));
+      expect(controller.getTitle(), completion('Web Title'));
+    });
+
+    test('currentUrl', () {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      when(mockWebView.getUrl())
+          .thenAnswer((_) => Future<String>.value('myUrl.com'));
+      expect(controller.currentUrl(), completion('myUrl.com'));
+    });
+
+    test('scrollTo', () async {
+      final MockUIScrollView mockScrollView = MockUIScrollView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockScrollView: mockScrollView,
+      );
+
+      await controller.scrollTo(2, 4);
+      verify(mockScrollView.setContentOffset(const Point<double>(2.0, 4.0)));
+    });
+
+    test('scrollBy', () async {
+      final MockUIScrollView mockScrollView = MockUIScrollView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockScrollView: mockScrollView,
+      );
+
+      await controller.scrollBy(2, 4);
+      verify(mockScrollView.scrollBy(const Point<double>(2.0, 4.0)));
+    });
+
+    test('getScrollPosition', () {
+      final MockUIScrollView mockScrollView = MockUIScrollView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockScrollView: mockScrollView,
+      );
+
+      when(mockScrollView.getContentOffset()).thenAnswer(
+        (_) => Future<Point<double>>.value(const Point<double>(8.0, 16.0)),
+      );
+      expect(
+        controller.getScrollPosition(),
+        completion(const Point<double>(8.0, 16.0)),
+      );
+    });
+
+    test('disable zoom', () async {
+      final MockWKUserContentController mockUserContentController =
+          MockWKUserContentController();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockUserContentController: mockUserContentController,
+      );
+
+      await controller.enableZoom(false);
+
+      final WKUserScript zoomScript =
+          verify(mockUserContentController.addUserScript(captureAny))
+              .captured
+              .first as WKUserScript;
+      expect(zoomScript.isMainFrameOnly, isTrue);
+      expect(zoomScript.injectionTime, WKUserScriptInjectionTime.atDocumentEnd);
+      expect(
+        zoomScript.source,
+        "var meta = document.createElement('meta');\n"
+        "meta.name = 'viewport';\n"
+        "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, "
+        "user-scalable=no';\n"
+        "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);",
+      );
+    });
+
+    test('setBackgroundColor', () async {
+      final MockWKWebView mockWebView = MockWKWebView();
+      final MockUIScrollView mockScrollView = MockUIScrollView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+        mockScrollView: mockScrollView,
+      );
+
+      controller.setBackgroundColor(Colors.red);
+
+      verify(mockWebView.setOpaque(false));
+      verify(mockWebView.setBackgroundColor(Colors.transparent));
+      verify(mockScrollView.setBackgroundColor(Colors.red));
+    });
+
+    test('userAgent', () async {
+      final MockWKWebView mockWebView = MockWKWebView();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        createMockWebView: (_, {dynamic observeValue}) => mockWebView,
+      );
+
+      await controller.setUserAgent('MyUserAgent');
+      verify(mockWebView.setCustomUserAgent('MyUserAgent'));
+    });
+
+    test('enable JavaScript', () async {
+      final MockWKPreferences mockPreferences = MockWKPreferences();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockPreferences: mockPreferences,
+      );
+
+      await controller.setJavaScriptMode(JavaScriptMode.unrestricted);
+
+      verify(mockPreferences.setJavaScriptEnabled(true));
+    });
+
+    test('disable JavaScript', () async {
+      final MockWKPreferences mockPreferences = MockWKPreferences();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockPreferences: mockPreferences,
+      );
+
+      await controller.setJavaScriptMode(JavaScriptMode.disabled);
+
+      verify(mockPreferences.setJavaScriptEnabled(false));
+    });
+
+    test('clearCache', () {
+      final MockWKWebsiteDataStore mockWebsiteDataStore =
+          MockWKWebsiteDataStore();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockWebsiteDataStore: mockWebsiteDataStore,
+      );
+      when(
+        mockWebsiteDataStore.removeDataOfTypes(
+          <WKWebsiteDataType>{
+            WKWebsiteDataType.memoryCache,
+            WKWebsiteDataType.diskCache,
+            WKWebsiteDataType.offlineWebApplicationCache,
+          },
+          DateTime.fromMillisecondsSinceEpoch(0),
+        ),
+      ).thenAnswer((_) => Future<bool>.value(false));
+
+      expect(controller.clearCache(), completes);
+    });
+
+    test('clearLocalStorage', () {
+      final MockWKWebsiteDataStore mockWebsiteDataStore =
+          MockWKWebsiteDataStore();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockWebsiteDataStore: mockWebsiteDataStore,
+      );
+      when(
+        mockWebsiteDataStore.removeDataOfTypes(
+          <WKWebsiteDataType>{WKWebsiteDataType.localStorage},
+          DateTime.fromMillisecondsSinceEpoch(0),
+        ),
+      ).thenAnswer((_) => Future<bool>.value(false));
+
+      expect(controller.clearLocalStorage(), completes);
+    });
+
+    test('addJavaScriptChannel', () async {
+      final WebKitProxy webKitProxy = WebKitProxy(
+        createScriptMessageHandler: ({
+          required void Function(
+            WKUserContentController userContentController,
+            WKScriptMessage message,
+          )
+              didReceiveScriptMessage,
+        }) {
+          return WKScriptMessageHandler.detached(
+            didReceiveScriptMessage: didReceiveScriptMessage,
+          );
+        },
+      );
+
+      final WebKitJavaScriptChannelParams javaScriptChannelParams =
+          WebKitJavaScriptChannelParams(
+        name: 'name',
+        onMessageReceived: (JavaScriptMessage message) {},
+        webKitProxy: webKitProxy,
+      );
+
+      final MockWKUserContentController mockUserContentController =
+          MockWKUserContentController();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockUserContentController: mockUserContentController,
+      );
+
+      await controller.addJavaScriptChannel(javaScriptChannelParams);
+      verify(mockUserContentController.addScriptMessageHandler(
+        argThat(isA<WKScriptMessageHandler>()),
+        'name',
+      ));
+
+      final WKUserScript userScript =
+          verify(mockUserContentController.addUserScript(captureAny))
+              .captured
+              .single as WKUserScript;
+      expect(userScript.source, 'window.name = webkit.messageHandlers.name;');
+      expect(
+        userScript.injectionTime,
+        WKUserScriptInjectionTime.atDocumentStart,
+      );
+    });
+
+    test('removeJavaScriptChannel', () async {
+      final WebKitProxy webKitProxy = WebKitProxy(
+        createScriptMessageHandler: ({
+          required void Function(
+            WKUserContentController userContentController,
+            WKScriptMessage message,
+          )
+              didReceiveScriptMessage,
+        }) {
+          return WKScriptMessageHandler.detached(
+            didReceiveScriptMessage: didReceiveScriptMessage,
+          );
+        },
+      );
+
+      final WebKitJavaScriptChannelParams javaScriptChannelParams =
+          WebKitJavaScriptChannelParams(
+        name: 'name',
+        onMessageReceived: (JavaScriptMessage message) {},
+        webKitProxy: webKitProxy,
+      );
+
+      final MockWKUserContentController mockUserContentController =
+          MockWKUserContentController();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockUserContentController: mockUserContentController,
+      );
+
+      await controller.addJavaScriptChannel(javaScriptChannelParams);
+      reset(mockUserContentController);
+
+      await controller.removeJavaScriptChannel('name');
+
+      verify(mockUserContentController.removeAllUserScripts());
+      verify(mockUserContentController.removeScriptMessageHandler('name'));
+
+      verifyNoMoreInteractions(mockUserContentController);
+    });
+
+    test('removeJavaScriptChannel with zoom disabled', () async {
+      final WebKitProxy webKitProxy = WebKitProxy(
+        createScriptMessageHandler: ({
+          required void Function(
+            WKUserContentController userContentController,
+            WKScriptMessage message,
+          )
+              didReceiveScriptMessage,
+        }) {
+          return WKScriptMessageHandler.detached(
+            didReceiveScriptMessage: didReceiveScriptMessage,
+          );
+        },
+      );
+
+      final WebKitJavaScriptChannelParams javaScriptChannelParams =
+          WebKitJavaScriptChannelParams(
+        name: 'name',
+        onMessageReceived: (JavaScriptMessage message) {},
+        webKitProxy: webKitProxy,
+      );
+
+      final MockWKUserContentController mockUserContentController =
+          MockWKUserContentController();
+
+      final WebKitWebViewController controller = createControllerWithMocks(
+        mockUserContentController: mockUserContentController,
+      );
+
+      await controller.enableZoom(false);
+      await controller.addJavaScriptChannel(javaScriptChannelParams);
+      clearInteractions(mockUserContentController);
+      await controller.removeJavaScriptChannel('name');
+
+      final WKUserScript zoomScript =
+          verify(mockUserContentController.addUserScript(captureAny))
+              .captured
+              .first as WKUserScript;
+      expect(zoomScript.isMainFrameOnly, isTrue);
+      expect(zoomScript.injectionTime, WKUserScriptInjectionTime.atDocumentEnd);
+      expect(
+        zoomScript.source,
+        "var meta = document.createElement('meta');\n"
+        "meta.name = 'viewport';\n"
+        "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, "
+        "user-scalable=no';\n"
+        "var head = document.getElementsByTagName('head')[0];head.appendChild(meta);",
+      );
+    });
+  });
+
+  group('WebKitJavaScriptChannelParams', () {
+    test('onMessageReceived', () async {
+      late final WKScriptMessageHandler messageHandler;
+
+      final WebKitProxy webKitProxy = WebKitProxy(
+        createScriptMessageHandler: ({
+          required void Function(
+            WKUserContentController userContentController,
+            WKScriptMessage message,
+          )
+              didReceiveScriptMessage,
+        }) {
+          messageHandler = WKScriptMessageHandler.detached(
+            didReceiveScriptMessage: didReceiveScriptMessage,
+          );
+          return messageHandler;
+        },
+      );
+
+      late final String callbackMessage;
+      WebKitJavaScriptChannelParams(
+        name: 'name',
+        onMessageReceived: (JavaScriptMessage message) {
+          callbackMessage = message.message;
+        },
+        webKitProxy: webKitProxy,
+      );
+
+      messageHandler.didReceiveScriptMessage(
+        MockWKUserContentController(),
+        const WKScriptMessage(name: 'name', body: 'myMessage'),
+      );
+
+      expect(callbackMessage, 'myMessage');
+    });
+  });
+}
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.mocks.dart
new file mode 100644
index 0000000..6138fdd
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.mocks.dart
@@ -0,0 +1,414 @@
+// Mocks generated by Mockito 5.2.0 from annotations
+// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart.
+// Do not manually edit this file.
+
+import 'dart:async' as _i5;
+import 'dart:math' as _i2;
+import 'dart:ui' as _i6;
+
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'
+    as _i7;
+import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart' as _i3;
+import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i4;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+
+class _FakePoint_0<T extends num> extends _i1.Fake implements _i2.Point<T> {}
+
+class _FakeUIScrollView_1 extends _i1.Fake implements _i3.UIScrollView {}
+
+class _FakeWKPreferences_2 extends _i1.Fake implements _i4.WKPreferences {}
+
+class _FakeWKUserContentController_3 extends _i1.Fake
+    implements _i4.WKUserContentController {}
+
+class _FakeWKHttpCookieStore_4 extends _i1.Fake
+    implements _i4.WKHttpCookieStore {}
+
+class _FakeWKWebsiteDataStore_5 extends _i1.Fake
+    implements _i4.WKWebsiteDataStore {}
+
+class _FakeWKWebViewConfiguration_6 extends _i1.Fake
+    implements _i4.WKWebViewConfiguration {}
+
+class _FakeWKWebView_7 extends _i1.Fake implements _i4.WKWebView {}
+
+/// A class which mocks [UIScrollView].
+///
+/// See the documentation for Mockito's code generation for more information.
+// ignore: must_be_immutable
+class MockUIScrollView extends _i1.Mock implements _i3.UIScrollView {
+  MockUIScrollView() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i5.Future<_i2.Point<double>> getContentOffset() => (super.noSuchMethod(
+          Invocation.method(#getContentOffset, []),
+          returnValue: Future<_i2.Point<double>>.value(_FakePoint_0<double>()))
+      as _i5.Future<_i2.Point<double>>);
+  @override
+  _i5.Future<void> scrollBy(_i2.Point<double>? offset) =>
+      (super.noSuchMethod(Invocation.method(#scrollBy, [offset]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setContentOffset(_i2.Point<double>? offset) =>
+      (super.noSuchMethod(Invocation.method(#setContentOffset, [offset]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i3.UIScrollView copy() => (super.noSuchMethod(Invocation.method(#copy, []),
+      returnValue: _FakeUIScrollView_1()) as _i3.UIScrollView);
+  @override
+  _i5.Future<void> setBackgroundColor(_i6.Color? color) =>
+      (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setOpaque(bool? opaque) =>
+      (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> addObserver(_i7.NSObject? observer,
+          {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) =>
+      (super.noSuchMethod(
+          Invocation.method(
+              #addObserver, [observer], {#keyPath: keyPath, #options: options}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> removeObserver(_i7.NSObject? observer, {String? keyPath}) =>
+      (super.noSuchMethod(
+          Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+}
+
+/// A class which mocks [WKPreferences].
+///
+/// See the documentation for Mockito's code generation for more information.
+// ignore: must_be_immutable
+class MockWKPreferences extends _i1.Mock implements _i4.WKPreferences {
+  MockWKPreferences() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i5.Future<void> setJavaScriptEnabled(bool? enabled) =>
+      (super.noSuchMethod(Invocation.method(#setJavaScriptEnabled, [enabled]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i4.WKPreferences copy() => (super.noSuchMethod(Invocation.method(#copy, []),
+      returnValue: _FakeWKPreferences_2()) as _i4.WKPreferences);
+  @override
+  _i5.Future<void> addObserver(_i7.NSObject? observer,
+          {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) =>
+      (super.noSuchMethod(
+          Invocation.method(
+              #addObserver, [observer], {#keyPath: keyPath, #options: options}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> removeObserver(_i7.NSObject? observer, {String? keyPath}) =>
+      (super.noSuchMethod(
+          Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+}
+
+/// A class which mocks [WKUserContentController].
+///
+/// See the documentation for Mockito's code generation for more information.
+// ignore: must_be_immutable
+class MockWKUserContentController extends _i1.Mock
+    implements _i4.WKUserContentController {
+  MockWKUserContentController() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i5.Future<void> addScriptMessageHandler(
+          _i4.WKScriptMessageHandler? handler, String? name) =>
+      (super.noSuchMethod(
+          Invocation.method(#addScriptMessageHandler, [handler, name]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> removeScriptMessageHandler(String? name) => (super
+      .noSuchMethod(Invocation.method(#removeScriptMessageHandler, [name]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> removeAllScriptMessageHandlers() => (super.noSuchMethod(
+      Invocation.method(#removeAllScriptMessageHandlers, []),
+      returnValue: Future<void>.value(),
+      returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> addUserScript(_i4.WKUserScript? userScript) =>
+      (super.noSuchMethod(Invocation.method(#addUserScript, [userScript]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> removeAllUserScripts() =>
+      (super.noSuchMethod(Invocation.method(#removeAllUserScripts, []),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i4.WKUserContentController copy() =>
+      (super.noSuchMethod(Invocation.method(#copy, []),
+              returnValue: _FakeWKUserContentController_3())
+          as _i4.WKUserContentController);
+  @override
+  _i5.Future<void> addObserver(_i7.NSObject? observer,
+          {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) =>
+      (super.noSuchMethod(
+          Invocation.method(
+              #addObserver, [observer], {#keyPath: keyPath, #options: options}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> removeObserver(_i7.NSObject? observer, {String? keyPath}) =>
+      (super.noSuchMethod(
+          Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+}
+
+/// A class which mocks [WKWebsiteDataStore].
+///
+/// See the documentation for Mockito's code generation for more information.
+// ignore: must_be_immutable
+class MockWKWebsiteDataStore extends _i1.Mock
+    implements _i4.WKWebsiteDataStore {
+  MockWKWebsiteDataStore() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i4.WKHttpCookieStore get httpCookieStore =>
+      (super.noSuchMethod(Invocation.getter(#httpCookieStore),
+          returnValue: _FakeWKHttpCookieStore_4()) as _i4.WKHttpCookieStore);
+  @override
+  _i5.Future<bool> removeDataOfTypes(
+          Set<_i4.WKWebsiteDataType>? dataTypes, DateTime? since) =>
+      (super.noSuchMethod(
+          Invocation.method(#removeDataOfTypes, [dataTypes, since]),
+          returnValue: Future<bool>.value(false)) as _i5.Future<bool>);
+  @override
+  _i4.WKWebsiteDataStore copy() =>
+      (super.noSuchMethod(Invocation.method(#copy, []),
+          returnValue: _FakeWKWebsiteDataStore_5()) as _i4.WKWebsiteDataStore);
+  @override
+  _i5.Future<void> addObserver(_i7.NSObject? observer,
+          {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) =>
+      (super.noSuchMethod(
+          Invocation.method(
+              #addObserver, [observer], {#keyPath: keyPath, #options: options}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> removeObserver(_i7.NSObject? observer, {String? keyPath}) =>
+      (super.noSuchMethod(
+          Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+}
+
+/// A class which mocks [WKWebView].
+///
+/// See the documentation for Mockito's code generation for more information.
+// ignore: must_be_immutable
+class MockWKWebView extends _i1.Mock implements _i4.WKWebView {
+  MockWKWebView() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i4.WKWebViewConfiguration get configuration =>
+      (super.noSuchMethod(Invocation.getter(#configuration),
+              returnValue: _FakeWKWebViewConfiguration_6())
+          as _i4.WKWebViewConfiguration);
+  @override
+  _i3.UIScrollView get scrollView =>
+      (super.noSuchMethod(Invocation.getter(#scrollView),
+          returnValue: _FakeUIScrollView_1()) as _i3.UIScrollView);
+  @override
+  _i5.Future<void> setUIDelegate(_i4.WKUIDelegate? delegate) =>
+      (super.noSuchMethod(Invocation.method(#setUIDelegate, [delegate]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setNavigationDelegate(_i4.WKNavigationDelegate? delegate) =>
+      (super.noSuchMethod(Invocation.method(#setNavigationDelegate, [delegate]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<String?> getUrl() =>
+      (super.noSuchMethod(Invocation.method(#getUrl, []),
+          returnValue: Future<String?>.value()) as _i5.Future<String?>);
+  @override
+  _i5.Future<double> getEstimatedProgress() =>
+      (super.noSuchMethod(Invocation.method(#getEstimatedProgress, []),
+          returnValue: Future<double>.value(0.0)) as _i5.Future<double>);
+  @override
+  _i5.Future<void> loadRequest(_i7.NSUrlRequest? request) =>
+      (super.noSuchMethod(Invocation.method(#loadRequest, [request]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> loadHtmlString(String? string, {String? baseUrl}) =>
+      (super.noSuchMethod(
+          Invocation.method(#loadHtmlString, [string], {#baseUrl: baseUrl}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> loadFileUrl(String? url, {String? readAccessUrl}) =>
+      (super.noSuchMethod(
+          Invocation.method(
+              #loadFileUrl, [url], {#readAccessUrl: readAccessUrl}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> loadFlutterAsset(String? key) =>
+      (super.noSuchMethod(Invocation.method(#loadFlutterAsset, [key]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<bool> canGoBack() =>
+      (super.noSuchMethod(Invocation.method(#canGoBack, []),
+          returnValue: Future<bool>.value(false)) as _i5.Future<bool>);
+  @override
+  _i5.Future<bool> canGoForward() =>
+      (super.noSuchMethod(Invocation.method(#canGoForward, []),
+          returnValue: Future<bool>.value(false)) as _i5.Future<bool>);
+  @override
+  _i5.Future<void> goBack() =>
+      (super.noSuchMethod(Invocation.method(#goBack, []),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> goForward() =>
+      (super.noSuchMethod(Invocation.method(#goForward, []),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> reload() =>
+      (super.noSuchMethod(Invocation.method(#reload, []),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<String?> getTitle() =>
+      (super.noSuchMethod(Invocation.method(#getTitle, []),
+          returnValue: Future<String?>.value()) as _i5.Future<String?>);
+  @override
+  _i5.Future<void> setAllowsBackForwardNavigationGestures(bool? allow) =>
+      (super.noSuchMethod(
+          Invocation.method(#setAllowsBackForwardNavigationGestures, [allow]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setCustomUserAgent(String? userAgent) =>
+      (super.noSuchMethod(Invocation.method(#setCustomUserAgent, [userAgent]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<Object?> evaluateJavaScript(String? javaScriptString) => (super
+      .noSuchMethod(Invocation.method(#evaluateJavaScript, [javaScriptString]),
+          returnValue: Future<Object?>.value()) as _i5.Future<Object?>);
+  @override
+  _i4.WKWebView copy() => (super.noSuchMethod(Invocation.method(#copy, []),
+      returnValue: _FakeWKWebView_7()) as _i4.WKWebView);
+  @override
+  _i5.Future<void> setBackgroundColor(_i6.Color? color) =>
+      (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setOpaque(bool? opaque) =>
+      (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> addObserver(_i7.NSObject? observer,
+          {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) =>
+      (super.noSuchMethod(
+          Invocation.method(
+              #addObserver, [observer], {#keyPath: keyPath, #options: options}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> removeObserver(_i7.NSObject? observer, {String? keyPath}) =>
+      (super.noSuchMethod(
+          Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+}
+
+/// A class which mocks [WKWebViewConfiguration].
+///
+/// See the documentation for Mockito's code generation for more information.
+// ignore: must_be_immutable
+class MockWKWebViewConfiguration extends _i1.Mock
+    implements _i4.WKWebViewConfiguration {
+  MockWKWebViewConfiguration() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i4.WKUserContentController get userContentController =>
+      (super.noSuchMethod(Invocation.getter(#userContentController),
+              returnValue: _FakeWKUserContentController_3())
+          as _i4.WKUserContentController);
+  @override
+  _i4.WKPreferences get preferences =>
+      (super.noSuchMethod(Invocation.getter(#preferences),
+          returnValue: _FakeWKPreferences_2()) as _i4.WKPreferences);
+  @override
+  _i4.WKWebsiteDataStore get websiteDataStore =>
+      (super.noSuchMethod(Invocation.getter(#websiteDataStore),
+          returnValue: _FakeWKWebsiteDataStore_5()) as _i4.WKWebsiteDataStore);
+  @override
+  _i5.Future<void> setAllowsInlineMediaPlayback(bool? allow) => (super
+      .noSuchMethod(Invocation.method(#setAllowsInlineMediaPlayback, [allow]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setMediaTypesRequiringUserActionForPlayback(
+          Set<_i4.WKAudiovisualMediaType>? types) =>
+      (super.noSuchMethod(
+          Invocation.method(
+              #setMediaTypesRequiringUserActionForPlayback, [types]),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i4.WKWebViewConfiguration copy() =>
+      (super.noSuchMethod(Invocation.method(#copy, []),
+              returnValue: _FakeWKWebViewConfiguration_6())
+          as _i4.WKWebViewConfiguration);
+  @override
+  _i5.Future<void> addObserver(_i7.NSObject? observer,
+          {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) =>
+      (super.noSuchMethod(
+          Invocation.method(
+              #addObserver, [observer], {#keyPath: keyPath, #options: options}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+  @override
+  _i5.Future<void> removeObserver(_i7.NSObject? observer, {String? keyPath}) =>
+      (super.noSuchMethod(
+          Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}),
+          returnValue: Future<void>.value(),
+          returnValueForMissingStub: Future<void>.value()) as _i5.Future<void>);
+}