[webview_flutter] Copies app-facing implementation of webview_flutter from v4_webview (#6856)

* copy code from v4_webview

* version bump and readme update

* work towards better readme

* improvements

* more readme progress

* improvements

* fix main and update more readme

* excerpt changes and more 3.0 diffs

* cookie manager update

* remove packages from exclude list

* lint

* better range

* isForMainFrame

* load page after waiting for widget

* fix integration tests

* improve readme a bit

* collapse changelong. update platform-specific wording. include in excerpt tests

* use platform implementation packages

* include missing exports

* PR comments

* correct spelling

* interface dev dependency

* move other usage above migration

* remove interface classes
diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md
index f278cf9..329ce48 100644
--- a/packages/webview_flutter/webview_flutter/CHANGELOG.md
+++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md
@@ -1,11 +1,11 @@
-## NEXT
+## 4.0.0
 
-* Updates code for `no_leading_underscores_for_local_identifiers` lint.
-* Updates minimum Flutter version to 2.10.
-* Fixes avoid_redundant_argument_values lint warnings and minor typos.
-* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231).
+* **BREAKING CHANGE** Updates implementation to use the `2.0.0` release of
+  `webview_flutter_platform_interface`. See `Usage` section in the README for updated usage. See
+  `Migrating from 3.0 to 4.0` section in the README for details on migrating to this version.
+* Updates minimum Flutter version to 3.0.0.
+* Updates code for new analysis options.
 * Updates references to the obsolete master branch.
-* Fixes typo from lowercase to uppercase.
 
 ## 3.0.4
 
diff --git a/packages/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md
index ffe9144..98f7b66 100644
--- a/packages/webview_flutter/webview_flutter/README.md
+++ b/packages/webview_flutter/webview_flutter/README.md
@@ -1,10 +1,12 @@
 # WebView for Flutter
 
+<?code-excerpt path-base="excerpts/packages/webview_flutter_example"?>
+
 [![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dev/packages/webview_flutter)
 
 A Flutter plugin that provides a WebView widget.
 
-On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview);
+On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview).
 On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView).
 
 |             | Android        | iOS  |
@@ -12,30 +14,60 @@
 | **Support** | SDK 19+ or 20+ | 9.0+ |
 
 ## Usage
-Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). If you are targeting Android, make sure to read the *Android Platform Views* section below to choose the platform view mode that best suits your needs.
+Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://pub.dev/packages/webview_flutter/install).
 
-You can now include a WebView widget in your widget tree. See the
-[WebView](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebView-class.html)
-widget's Dartdoc for more details on how to use the widget.
+You can now display a WebView by:
 
-## Android Platform Views
+1. Instantiating a [WebViewController](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewController-class.html).
+
+<?code-excerpt "simple_example.dart (webview_controller)"?>
+```dart
+controller = WebViewController()
+  ..setJavaScriptMode(JavaScriptMode.unrestricted)
+  ..setBackgroundColor(const Color(0x00000000))
+  ..setNavigationDelegate(
+    NavigationDelegate(
+      onProgress: (int progress) {
+        // Update loading bar.
+      },
+      onPageStarted: (String url) {},
+      onPageFinished: (String url) {},
+      onWebResourceError: (WebResourceError error) {},
+      onNavigationRequest: (NavigationRequest request) {
+        if (request.url.startsWith('https://www.youtube.com/')) {
+          return NavigationDecision.prevent;
+        }
+        return NavigationDecision.navigate;
+      },
+    ),
+  )
+  ..loadRequest(Uri.parse('https://flutter.dev'));
+```
+
+2. Passing the controller to a [WebViewWidget](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html).
+
+<?code-excerpt "simple_example.dart (webview_widget)"?>
+```dart
+@override
+Widget build(BuildContext context) {
+  return Scaffold(
+    appBar: AppBar(title: const Text('Flutter Simple Example')),
+    body: WebViewWidget(controller: controller),
+  );
+}
+```
+
+See the Dartdocs for [WebViewController](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewController-class.html)
+and [WebViewWidget](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html)
+for more details.
+
+### Android Platform Views
+
 This plugin uses
 [Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views) to embed
-the Android’s webview within the Flutter app. It supports two modes:
-*hybrid composition* (the current default) and *virtual display*.
+the Android’s WebView within the Flutter app.
 
-Here are some points to consider when choosing between the two:
-
-* *Hybrid composition* has built-in keyboard support while *virtual display* has multiple
-[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22).
-* *Hybrid composition* requires Android SDK 19+ while *virtual display* requires Android SDK 20+.
-* *Hybrid composition* and *virtual display* have different
-  [performance tradeoffs](https://flutter.dev/docs/development/platform-integration/platform-views#performance).
-
-
-### Using Hybrid Composition
-
-The mode is currently enabled by default. You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` if it was previously lower than 19:
+You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` if it was previously lower than 19:
 
 ```groovy
 android {
@@ -45,47 +77,67 @@
 }
 ```
 
-### Using Virtual displays
+### Platform-Specific Features
 
-1. Set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 20):
+Many classes have a subclass or an underlying implementation that provides access to platform-specific
+features.
 
-    ```groovy
-    android {
-        defaultConfig {
-            minSdkVersion 20
-        }
-    }
-    ```
+To access platform-specific features, start by adding the platform implementation packages to your
+app or package:
 
-2. Set `WebView.platform = AndroidWebView();` in `initState()`.
-    For example:
+* **Android**: [webview_flutter_android](https://pub.dev/packages/webview_flutter_android/install)
+* **iOS**: [webview_flutter_wkwebview](https://pub.dev/packages/webview_flutter_wkwebview/install)
 
-    ```dart
-    import 'dart:io';
+Next, add the imports of the implementation packages to your app or package:
 
-    import 'package:webview_flutter/webview_flutter.dart';
+<?code-excerpt "main.dart (platform_imports)"?>
+```dart
+// Import for Android features.
+import 'package:webview_flutter_android/webview_flutter_android.dart';
+// Import for iOS features.
+import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
+```
 
-    class WebViewExample extends StatefulWidget {
-      @override
-      WebViewExampleState createState() => WebViewExampleState();
-    }
+Now, additional features can be accessed through the platform implementations. Classes
+`WebViewController`, `WebViewWidget`, `NavigationDelegate`, and `WebViewCookieManager` pass their
+functionality to a class provided by the current platform. Below are a couple of ways to access
+additional functionality provided by the platform and is followed by an example.
 
-    class WebViewExampleState extends State<WebViewExample> {
-      @override
-      void initState() {
-        super.initState();
-        // Enable virtual display.
-        if (Platform.isAndroid) WebView.platform = AndroidWebView();
-      }
+1. Pass a creation params class provided by a platform implementation to a `fromPlatformCreationParams`
+   constructor (e.g. `WebViewController.fromPlatformCreationParams`,
+   `WebViewWidget.fromPlatformCreationParams`, etc.).
+2. Call methods on a platform implementation of a class by using the `platform` field (e.g.
+   `WebViewController.platform`, `WebViewWidget.platform`, etc.).
 
-      @override
-      Widget build(BuildContext context) {
-        return WebView(
-          initialUrl: 'https://flutter.dev',
-        );
-      }
-    }
-    ```
+Below is an example of setting additional iOS and Android parameters on the `WebViewController`.
+
+<?code-excerpt "main.dart (platform_features)"?>
+```dart
+late final PlatformWebViewControllerCreationParams params;
+if (WebViewPlatform.instance is WebKitWebViewPlatform) {
+  params = WebKitWebViewControllerCreationParams(
+    allowsInlineMediaPlayback: true,
+    mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
+  );
+} else {
+  params = const PlatformWebViewControllerCreationParams();
+}
+
+final WebViewController controller =
+    WebViewController.fromPlatformCreationParams(params);
+// ···
+if (controller.platform is AndroidWebViewController) {
+  AndroidWebViewController.enableDebugging(true);
+  (controller.platform as AndroidWebViewController)
+      .setMediaPlaybackRequiresUserGesture(false);
+}
+```
+
+See https://pub.dev/documentation/webview_flutter_android/latest/webview_flutter_android/webview_flutter_android-library.html
+for more details on Android features.
+
+See https://pub.dev/documentation/webview_flutter_wkwebview/latest/webview_flutter_wkwebview/webview_flutter_wkwebview-library.html
+for more details on iOS features.
 
 ### Enable Material Components for Android
 
@@ -95,4 +147,76 @@
 ### Setting custom headers on POST requests
 
 Currently, setting custom headers when making a post request with the WebViewController's `loadRequest` method is not supported on Android.
-If you require this functionality, a workaround is to make the request manually, and then load the response data using `loadHTMLString` instead.
+If you require this functionality, a workaround is to make the request manually, and then load the response data using `loadHtmlString` instead.
+
+## Migrating from 3.0 to 4.0
+
+### Instantiating WebViewController
+
+In version 3.0 and below, `WebViewController` could only be retrieved in a callback after the
+`WebView` was added to the widget tree. Now, `WebViewController` must be instantiated and can be
+used before it is added to the widget tree. See `Usage` section above for an example.
+
+### Replacing WebView Functionality
+
+The `WebView` class has been removed and its functionality has been split into `WebViewController`
+and `WebViewWidget`.
+
+`WebViewController` handles all functionality that is associated with the underlying web view
+provided by each platform. (e.g., loading a url, setting the background color of the underlying
+platform view, or clearing the cache).
+
+`WebViewWidget` takes a `WebViewController` and handles all Flutter widget related functionality
+(e.g., layout direction, gesture recognizers).
+
+See the Dartdocs for [WebViewController](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewController-class.html)
+and [WebViewWidget](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html)
+for more details.
+
+### PlatformView Implementation on Android
+
+The PlatformView implementation for Android is currently no longer configurable. It uses Texture
+Layer Hybrid Composition on versions 23+ and automatically fallbacks to Hybrid Composition for
+version 19-23. See https://github.com/flutter/flutter/issues/108106 for progress on manually
+switching to Hybrid Composition on versions 23+.
+
+### API Changes
+
+Below is a non-exhaustive list of changes to the API:
+
+* `WebViewController.clearCache` no longer clears local storage. Please use
+  `WebViewController.clearLocalStorage`.
+* `WebViewController.clearCache` no longer reloads the page.
+* `WebViewController.loadUrl` has been removed. Please use `WebViewController.loadRequest`.
+* `WebViewController.evaluateJavascript` has been removed. Please use
+  `WebViewController.runJavaScript` or `WebViewController.runJavaScriptReturningResult`.
+* `WebViewController.getScrollX` and `WebViewController.getScrollY` have been removed and have
+  been replaced by `WebViewController.getScrollPosition`.
+* `WebViewController.runJavaScriptReturningResult` now returns an `Object` and not a `String`. This
+  will attempt to return a `bool` or `num` if the return value can be parsed.
+* `CookieManager` is replaced by `WebViewCookieManager`.
+* `NavigationDelegate.onWebResourceError` callback includes errors that are not from the main frame.
+   Use the `WebResourceError.isForMainFrame` field to filter errors.
+* The following fields from `WebView` have been moved to `NavigationDelegate`. They can be added to
+  a WebView with `WebViewController.setNavigationDelegate`.
+  * `WebView.navigationDelegate` -> `NavigationDelegate.onNavigationRequest`
+  * `WebView.onPageStarted` -> `NavigationDelegate.onPageStarted`
+  * `WebView.onPageFinished` -> `NavigationDelegate.onPageFinished`
+  * `WebView.onProgress` -> `NavigationDelegate.onProgress`
+  * `WebView.onWebResourceError` -> `NavigationDelegate.onWebResourceError`
+* The following fields from `WebView` have been moved to `WebViewController`:
+  * `WebView.javascriptMode` -> `WebViewController.setJavaScriptMode`
+  * `WebView.javascriptChannels` ->
+    `WebViewController.addJavaScriptChannel`/`WebViewController.removeJavaScriptChannel`
+  * `WebView.zoomEnabled` -> `WebViewController.enableZoom`
+  * `WebView.userAgent` -> `WebViewController.setUserAgent`
+  * `WebView.backgroundColor` -> `WebViewController.setBackgroundColor`
+* The following features have been moved to an Android implementation class. See section
+  `Platform-Specific Features` for details on accessing Android platform specific features.
+  * `WebView.debuggingEnabled` -> `static AndroidWebViewController.enableDebugging`
+  * `WebView.initialMediaPlaybackPolicy` -> `AndroidWebViewController.setMediaPlaybackRequiresUserGesture`
+* The following features have been moved to an iOS implementation class. See section
+  `Platform-Specific Features` for details on accessing iOS platform specific features.
+  * `WebView.gestureNavigationEnabled` -> `WebKitWebViewController.setAllowsBackForwardNavigationGestures`
+  * `WebView.initialMediaPlaybackPolicy` -> `WebKitWebViewControllerCreationParams.mediaTypesRequiringUserAction`
+  * `WebView.allowsInlineMediaPlayback` -> `WebKitWebViewControllerCreationParams.allowsInlineMediaPlayback`
diff --git a/packages/webview_flutter/webview_flutter/example/build.excerpt.yaml b/packages/webview_flutter/webview_flutter/example/build.excerpt.yaml
new file mode 100644
index 0000000..46c1e75
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/example/build.excerpt.yaml
@@ -0,0 +1,15 @@
+targets:
+  $default:
+    sources:
+      include:
+        - lib/**
+        # Some default includes that aren't really used here but will prevent
+        # false-negative warnings:
+        - $package$
+        - lib/$lib$
+      exclude:
+        - '**/.*/**'
+        - '**/build/**'
+    builders:
+      code_excerpter|code_excerpter:
+        enabled: true
\ No newline at end of file
diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/legacy/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/legacy/webview_flutter_test.dart
new file mode 100644
index 0000000..1453910
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/example/integration_test/legacy/webview_flutter_test.dart
@@ -0,0 +1,1382 @@
+// 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.
+
+// This test is run using `flutter drive` by the CI (see /script/tool/README.md
+// in this repository for details on driving that tooling manually), but can
+// also be run using `flutter test` directly during development.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+// 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/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:webview_flutter/src/webview_flutter_legacy.dart';
+
+Future<void> main() async {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0);
+  server.forEach((HttpRequest request) {
+    if (request.uri.path == '/hello.txt') {
+      request.response.writeln('Hello, world.');
+    } else if (request.uri.path == '/secondary.txt') {
+      request.response.writeln('How are you today?');
+    } else if (request.uri.path == '/headers') {
+      request.response.writeln('${request.headers}');
+    } else if (request.uri.path == '/favicon.ico') {
+      request.response.statusCode = HttpStatus.notFound;
+    } else {
+      fail('unexpected request: ${request.method} ${request.uri}');
+    }
+    request.response.close();
+  });
+  final String prefixUrl = 'http://${server.address.address}:${server.port}';
+  final String primaryUrl = '$prefixUrl/hello.txt';
+  final String secondaryUrl = '$prefixUrl/secondary.txt';
+  final String headersUrl = '$prefixUrl/headers';
+
+  testWidgets('initialUrl', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final Completer<void> pageFinishedCompleter = Completer<void>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          initialUrl: primaryUrl,
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          onPageFinished: pageFinishedCompleter.complete,
+        ),
+      ),
+    );
+
+    final WebViewController controller = await controllerCompleter.future;
+    await pageFinishedCompleter.future;
+
+    final String? currentUrl = await controller.currentUrl();
+    expect(currentUrl, primaryUrl);
+  });
+
+  testWidgets('loadUrl', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final StreamController<String> pageLoads = StreamController<String>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          initialUrl: primaryUrl,
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          onPageFinished: (String url) {
+            pageLoads.add(url);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+
+    await controller.loadUrl(secondaryUrl);
+    await expectLater(
+      pageLoads.stream.firstWhere((String url) => url == secondaryUrl),
+      completion(secondaryUrl),
+    );
+  });
+
+  testWidgets('evaluateJavascript', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          initialUrl: primaryUrl,
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          javascriptMode: JavascriptMode.unrestricted,
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    // ignore: deprecated_member_use
+    final String result = await controller.evaluateJavascript('1 + 1');
+    expect(result, equals('2'));
+  });
+
+  testWidgets('loadUrl with headers', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final StreamController<String> pageStarts = StreamController<String>();
+    final StreamController<String> pageLoads = StreamController<String>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          initialUrl: primaryUrl,
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          javascriptMode: JavascriptMode.unrestricted,
+          onPageStarted: (String url) {
+            pageStarts.add(url);
+          },
+          onPageFinished: (String url) {
+            pageLoads.add(url);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    final Map<String, String> headers = <String, String>{
+      'test_header': 'flutter_test_header'
+    };
+    await controller.loadUrl(headersUrl, headers: headers);
+    final String? currentUrl = await controller.currentUrl();
+    expect(currentUrl, headersUrl);
+
+    await pageStarts.stream.firstWhere((String url) => url == currentUrl);
+    await pageLoads.stream.firstWhere((String url) => url == currentUrl);
+
+    final String content = await controller
+        .runJavascriptReturningResult('document.documentElement.innerText');
+    expect(content.contains('flutter_test_header'), isTrue);
+  });
+
+  testWidgets('JavascriptChannel', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final Completer<void> pageStarted = Completer<void>();
+    final Completer<void> pageLoaded = Completer<void>();
+    final Completer<String> channelCompleter = Completer<String>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          // This is the data URL for: '<!DOCTYPE html>'
+          initialUrl:
+              'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          javascriptMode: JavascriptMode.unrestricted,
+          javascriptChannels: <JavascriptChannel>{
+            JavascriptChannel(
+              name: 'Echo',
+              onMessageReceived: (JavascriptMessage message) {
+                channelCompleter.complete(message.message);
+              },
+            ),
+          },
+          onPageStarted: (String url) {
+            pageStarted.complete(null);
+          },
+          onPageFinished: (String url) {
+            pageLoaded.complete(null);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    await pageStarted.future;
+    await pageLoaded.future;
+
+    expect(channelCompleter.isCompleted, isFalse);
+    await controller.runJavascript('Echo.postMessage("hello");');
+
+    await expectLater(channelCompleter.future, completion('hello'));
+  });
+
+  testWidgets('resize webview', (WidgetTester tester) async {
+    final Completer<void> initialResizeCompleter = Completer<void>();
+    final Completer<void> buttonTapResizeCompleter = Completer<void>();
+    final Completer<void> onPageFinished = Completer<void>();
+
+    bool resizeButtonTapped = false;
+    await tester.pumpWidget(ResizableWebView(
+      onResize: (_) {
+        if (resizeButtonTapped) {
+          buttonTapResizeCompleter.complete();
+        } else {
+          initialResizeCompleter.complete();
+        }
+      },
+      onPageFinished: () => onPageFinished.complete(),
+    ));
+    await onPageFinished.future;
+    // Wait for a potential call to resize after page is loaded.
+    await initialResizeCompleter.future.timeout(
+      const Duration(seconds: 3),
+      onTimeout: () => null,
+    );
+
+    resizeButtonTapped = true;
+    await tester.tap(find.byKey(const ValueKey<String>('resizeButton')));
+    await tester.pumpAndSettle();
+    expect(buttonTapResizeCompleter.future, completes);
+  });
+
+  testWidgets('set custom userAgent', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter1 =
+        Completer<WebViewController>();
+    final GlobalKey globalKey = GlobalKey();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: globalKey,
+          initialUrl: 'about:blank',
+          javascriptMode: JavascriptMode.unrestricted,
+          userAgent: 'Custom_User_Agent1',
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter1.complete(controller);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller1 = await controllerCompleter1.future;
+    final String customUserAgent1 = await _getUserAgent(controller1);
+    expect(customUserAgent1, 'Custom_User_Agent1');
+    // rebuild the WebView with a different user agent.
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: globalKey,
+          initialUrl: 'about:blank',
+          javascriptMode: JavascriptMode.unrestricted,
+          userAgent: 'Custom_User_Agent2',
+        ),
+      ),
+    );
+
+    final String customUserAgent2 = await _getUserAgent(controller1);
+    expect(customUserAgent2, 'Custom_User_Agent2');
+  });
+
+  testWidgets('use default platform userAgent after webView is rebuilt',
+      (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final GlobalKey globalKey = GlobalKey();
+    // Build the webView with no user agent to get the default platform user agent.
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: globalKey,
+          initialUrl: primaryUrl,
+          javascriptMode: JavascriptMode.unrestricted,
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    final String defaultPlatformUserAgent = await _getUserAgent(controller);
+    // rebuild the WebView with a custom user agent.
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: globalKey,
+          initialUrl: 'about:blank',
+          javascriptMode: JavascriptMode.unrestricted,
+          userAgent: 'Custom_User_Agent',
+        ),
+      ),
+    );
+    final String customUserAgent = await _getUserAgent(controller);
+    expect(customUserAgent, 'Custom_User_Agent');
+    // rebuilds the WebView with no user agent.
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: globalKey,
+          initialUrl: 'about:blank',
+          javascriptMode: JavascriptMode.unrestricted,
+        ),
+      ),
+    );
+
+    final String customUserAgent2 = await _getUserAgent(controller);
+    expect(customUserAgent2, defaultPlatformUserAgent);
+  });
+
+  group('Video playback policy', () {
+    late String videoTestBase64;
+    setUpAll(() async {
+      final ByteData videoData =
+          await rootBundle.load('assets/sample_video.mp4');
+      final String base64VideoData =
+          base64Encode(Uint8List.view(videoData.buffer));
+      final String videoTest = '''
+        <!DOCTYPE html><html>
+        <head><title>Video auto play</title>
+          <script type="text/javascript">
+            function play() {
+              var video = document.getElementById("video");
+              video.play();
+              video.addEventListener('timeupdate', videoTimeUpdateHandler, false);
+            }
+            function videoTimeUpdateHandler(e) {
+              var video = document.getElementById("video");
+              VideoTestTime.postMessage(video.currentTime);
+            }
+            function isPaused() {
+              var video = document.getElementById("video");
+              return video.paused;
+            }
+            function isFullScreen() {
+              var video = document.getElementById("video");
+              return video.webkitDisplayingFullscreen;
+            }
+          </script>
+        </head>
+        <body onload="play();">
+        <video controls playsinline autoplay id="video">
+          <source src="data:video/mp4;charset=utf-8;base64,$base64VideoData">
+        </video>
+        </body>
+        </html>
+      ''';
+      videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest));
+    });
+
+    testWidgets('Auto media playback', (WidgetTester tester) async {
+      Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      String isPaused =
+          await controller.runJavascriptReturningResult('isPaused();');
+      expect(isPaused, _webviewBool(false));
+
+      controllerCompleter = Completer<WebViewController>();
+      pageLoaded = Completer<void>();
+
+      // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+          ),
+        ),
+      );
+
+      controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      isPaused = await controller.runJavascriptReturningResult('isPaused();');
+      expect(isPaused, _webviewBool(true));
+    });
+
+    testWidgets('Changes to initialMediaPlaybackPolicy are ignored',
+        (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: key,
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      final WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      String isPaused =
+          await controller.runJavascriptReturningResult('isPaused();');
+      expect(isPaused, _webviewBool(false));
+
+      pageLoaded = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: key,
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+          ),
+        ),
+      );
+
+      await controller.reload();
+
+      await pageLoaded.future;
+
+      isPaused = await controller.runJavascriptReturningResult('isPaused();');
+      expect(isPaused, _webviewBool(false));
+    });
+
+    testWidgets('Video plays inline when allowsInlineMediaPlayback is true',
+        (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      final Completer<void> pageLoaded = Completer<void>();
+      final Completer<void> videoPlaying = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            javascriptChannels: <JavascriptChannel>{
+              JavascriptChannel(
+                name: 'VideoTestTime',
+                onMessageReceived: (JavascriptMessage message) {
+                  final double currentTime = double.parse(message.message);
+                  // Let it play for at least 1 second to make sure the related video's properties are set.
+                  if (currentTime > 1 && !videoPlaying.isCompleted) {
+                    videoPlaying.complete(null);
+                  }
+                },
+              ),
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+            allowsInlineMediaPlayback: true,
+          ),
+        ),
+      );
+      final WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      // Pump once to trigger the video play.
+      await tester.pump();
+
+      // Makes sure we get the correct event that indicates the video is actually playing.
+      await videoPlaying.future;
+
+      final String fullScreen =
+          await controller.runJavascriptReturningResult('isFullScreen();');
+      expect(fullScreen, _webviewBool(false));
+    });
+
+    // allowsInlineMediaPlayback is a noop on Android, so it is skipped.
+    testWidgets(
+        'Video plays full screen when allowsInlineMediaPlayback is false',
+        (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      final Completer<void> pageLoaded = Completer<void>();
+      final Completer<void> videoPlaying = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            javascriptChannels: <JavascriptChannel>{
+              JavascriptChannel(
+                name: 'VideoTestTime',
+                onMessageReceived: (JavascriptMessage message) {
+                  final double currentTime = double.parse(message.message);
+                  // Let it play for at least 1 second to make sure the related video's properties are set.
+                  if (currentTime > 1 && !videoPlaying.isCompleted) {
+                    videoPlaying.complete(null);
+                  }
+                },
+              ),
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      final WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      // Pump once to trigger the video play.
+      await tester.pump();
+
+      // Makes sure we get the correct event that indicates the video is actually playing.
+      await videoPlaying.future;
+
+      final String fullScreen =
+          await controller.runJavascriptReturningResult('isFullScreen();');
+      expect(fullScreen, _webviewBool(true));
+    }, skip: Platform.isAndroid);
+  });
+
+  group('Audio playback policy', () {
+    late String audioTestBase64;
+    setUpAll(() async {
+      final ByteData audioData =
+          await rootBundle.load('assets/sample_audio.ogg');
+      final String base64AudioData =
+          base64Encode(Uint8List.view(audioData.buffer));
+      final String audioTest = '''
+        <!DOCTYPE html><html>
+        <head><title>Audio auto play</title>
+          <script type="text/javascript">
+            function play() {
+              var audio = document.getElementById("audio");
+              audio.play();
+            }
+            function isPaused() {
+              var audio = document.getElementById("audio");
+              return audio.paused;
+            }
+          </script>
+        </head>
+        <body onload="play();">
+        <audio controls id="audio">
+          <source src="data:audio/ogg;charset=utf-8;base64,$base64AudioData">
+        </audio>
+        </body>
+        </html>
+      ''';
+      audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest));
+    });
+
+    testWidgets('Auto media playback', (WidgetTester tester) async {
+      Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageStarted = Completer<void>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      WebViewController controller = await controllerCompleter.future;
+      await pageStarted.future;
+      await pageLoaded.future;
+
+      String isPaused =
+          await controller.runJavascriptReturningResult('isPaused();');
+      expect(isPaused, _webviewBool(false));
+
+      controllerCompleter = Completer<WebViewController>();
+      pageStarted = Completer<void>();
+      pageLoaded = Completer<void>();
+
+      // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+          ),
+        ),
+      );
+
+      controller = await controllerCompleter.future;
+      await pageStarted.future;
+      await pageLoaded.future;
+
+      isPaused = await controller.runJavascriptReturningResult('isPaused();');
+      expect(isPaused, _webviewBool(true));
+    });
+
+    testWidgets('Changes to initialMediaPlaybackPolicy are ignored',
+        (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageStarted = Completer<void>();
+      Completer<void> pageLoaded = Completer<void>();
+
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: key,
+            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+          ),
+        ),
+      );
+      final WebViewController controller = await controllerCompleter.future;
+      await pageStarted.future;
+      await pageLoaded.future;
+
+      String isPaused =
+          await controller.runJavascriptReturningResult('isPaused();');
+      expect(isPaused, _webviewBool(false));
+
+      pageStarted = Completer<void>();
+      pageLoaded = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: key,
+            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageStarted: (String url) {
+              pageStarted.complete(null);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+          ),
+        ),
+      );
+
+      await controller.reload();
+
+      await pageStarted.future;
+      await pageLoaded.future;
+
+      isPaused = await controller.runJavascriptReturningResult('isPaused();');
+      expect(isPaused, _webviewBool(false));
+    });
+  });
+
+  testWidgets('getTitle', (WidgetTester tester) async {
+    const String getTitleTest = '''
+        <!DOCTYPE html><html>
+        <head><title>Some title</title>
+        </head>
+        <body>
+        </body>
+        </html>
+      ''';
+    final String getTitleTestBase64 =
+        base64Encode(const Utf8Encoder().convert(getTitleTest));
+    final Completer<void> pageStarted = Completer<void>();
+    final Completer<void> pageLoaded = Completer<void>();
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64',
+          javascriptMode: JavascriptMode.unrestricted,
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          onPageStarted: (String url) {
+            pageStarted.complete(null);
+          },
+          onPageFinished: (String url) {
+            pageLoaded.complete(null);
+          },
+        ),
+      ),
+    );
+
+    final WebViewController controller = await controllerCompleter.future;
+    await pageStarted.future;
+    await pageLoaded.future;
+
+    // On at least iOS, it does not appear to be guaranteed that the native
+    // code has the title when the page load completes. Execute some JavaScript
+    // before checking the title to ensure that the page has been fully parsed
+    // and processed.
+    await controller.runJavascript('1;');
+
+    final String? title = await controller.getTitle();
+    expect(title, 'Some title');
+  });
+
+  group('Programmatic Scroll', () {
+    testWidgets('setAndGetScrollPosition', (WidgetTester tester) async {
+      const String scrollTestPage = '''
+        <!DOCTYPE html>
+        <html>
+          <head>
+            <style>
+              body {
+                height: 100%;
+                width: 100%;
+              }
+              #container{
+                width:5000px;
+                height:5000px;
+            }
+            </style>
+          </head>
+          <body>
+            <div id="container"/>
+          </body>
+        </html>
+      ''';
+
+      final String scrollTestPageBase64 =
+          base64Encode(const Utf8Encoder().convert(scrollTestPage));
+
+      final Completer<void> pageLoaded = Completer<void>();
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            initialUrl:
+                'data:text/html;charset=utf-8;base64,$scrollTestPageBase64',
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            onPageFinished: (String url) {
+              pageLoaded.complete(null);
+            },
+          ),
+        ),
+      );
+
+      final WebViewController controller = await controllerCompleter.future;
+      await pageLoaded.future;
+
+      await tester.pumpAndSettle(const Duration(seconds: 3));
+
+      int scrollPosX = await controller.getScrollX();
+      int scrollPosY = await controller.getScrollY();
+
+      // Check scrollTo()
+      const int X_SCROLL = 123;
+      const int Y_SCROLL = 321;
+      // Get the initial position; this ensures that scrollTo is actually
+      // changing something, but also gives the native view's scroll position
+      // time to settle.
+      expect(scrollPosX, isNot(X_SCROLL));
+      expect(scrollPosX, isNot(Y_SCROLL));
+
+      await controller.scrollTo(X_SCROLL, Y_SCROLL);
+      scrollPosX = await controller.getScrollX();
+      scrollPosY = await controller.getScrollY();
+      expect(scrollPosX, X_SCROLL);
+      expect(scrollPosY, Y_SCROLL);
+
+      // Check scrollBy() (on top of scrollTo())
+      await controller.scrollBy(X_SCROLL, Y_SCROLL);
+      scrollPosX = await controller.getScrollX();
+      scrollPosY = await controller.getScrollY();
+      expect(scrollPosX, X_SCROLL * 2);
+      expect(scrollPosY, Y_SCROLL * 2);
+    });
+  });
+
+  // Minimal end-to-end testing of the legacy Android implementation.
+  group('AndroidWebView (virtual display)', () {
+    setUpAll(() {
+      WebView.platform = AndroidWebView();
+    });
+
+    tearDownAll(() {
+      WebView.platform = null;
+    });
+
+    testWidgets('initialUrl', (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      final Completer<void> pageFinishedCompleter = Completer<void>();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: primaryUrl,
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            onPageFinished: pageFinishedCompleter.complete,
+          ),
+        ),
+      );
+
+      final WebViewController controller = await controllerCompleter.future;
+      await pageFinishedCompleter.future;
+
+      final String? currentUrl = await controller.currentUrl();
+      expect(currentUrl, primaryUrl);
+    });
+  }, skip: !Platform.isAndroid);
+
+  group('NavigationDelegate', () {
+    const String blankPage = '<!DOCTYPE html><head></head><body></body></html>';
+    final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,'
+        '${base64Encode(const Utf8Encoder().convert(blankPage))}';
+
+    testWidgets('can allow requests', (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      final StreamController<String> pageLoads =
+          StreamController<String>.broadcast();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: blankPageEncoded,
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            navigationDelegate: (NavigationRequest request) {
+              return (request.url.contains('youtube.com'))
+                  ? NavigationDecision.prevent
+                  : NavigationDecision.navigate;
+            },
+            onPageFinished: (String url) => pageLoads.add(url),
+          ),
+        ),
+      );
+
+      await pageLoads.stream.first; // Wait for initial page load.
+      final WebViewController controller = await controllerCompleter.future;
+      await controller.runJavascript('location.href = "$secondaryUrl"');
+
+      await pageLoads.stream.first; // Wait for the next page load.
+      final String? currentUrl = await controller.currentUrl();
+      expect(currentUrl, secondaryUrl);
+    });
+
+    testWidgets('onWebResourceError', (WidgetTester tester) async {
+      final Completer<WebResourceError> errorCompleter =
+          Completer<WebResourceError>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: 'https://www.notawebsite..com',
+            onWebResourceError: (WebResourceError error) {
+              errorCompleter.complete(error);
+            },
+          ),
+        ),
+      );
+
+      final WebResourceError error = await errorCompleter.future;
+      expect(error, isNotNull);
+
+      if (Platform.isIOS) {
+        expect(error.domain, isNotNull);
+        expect(error.failingUrl, isNull);
+      } else if (Platform.isAndroid) {
+        expect(error.errorType, isNotNull);
+        expect(error.failingUrl?.startsWith('https://www.notawebsite..com'),
+            isTrue);
+      }
+    });
+
+    testWidgets('onWebResourceError is not called with valid url',
+        (WidgetTester tester) async {
+      final Completer<WebResourceError> errorCompleter =
+          Completer<WebResourceError>();
+      final Completer<void> pageFinishCompleter = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl:
+                'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
+            onWebResourceError: (WebResourceError error) {
+              errorCompleter.complete(error);
+            },
+            onPageFinished: (_) => pageFinishCompleter.complete(),
+          ),
+        ),
+      );
+
+      expect(errorCompleter.future, doesNotComplete);
+      await pageFinishCompleter.future;
+    });
+
+    testWidgets(
+      'onWebResourceError only called for main frame',
+      (WidgetTester tester) async {
+        const String iframeTest = '''
+        <!DOCTYPE html>
+        <html>
+        <head>
+          <title>WebResourceError test</title>
+        </head>
+        <body>
+          <iframe src="https://notawebsite..com"></iframe>
+        </body>
+        </html>
+       ''';
+        final String iframeTestBase64 =
+            base64Encode(const Utf8Encoder().convert(iframeTest));
+
+        final Completer<WebResourceError> errorCompleter =
+            Completer<WebResourceError>();
+        final Completer<void> pageFinishCompleter = Completer<void>();
+
+        await tester.pumpWidget(
+          Directionality(
+            textDirection: TextDirection.ltr,
+            child: WebView(
+              key: GlobalKey(),
+              initialUrl:
+                  'data:text/html;charset=utf-8;base64,$iframeTestBase64',
+              onWebResourceError: (WebResourceError error) {
+                errorCompleter.complete(error);
+              },
+              onPageFinished: (_) => pageFinishCompleter.complete(),
+            ),
+          ),
+        );
+
+        expect(errorCompleter.future, doesNotComplete);
+        await pageFinishCompleter.future;
+      },
+    );
+
+    testWidgets('can block requests', (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      final StreamController<String> pageLoads =
+          StreamController<String>.broadcast();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: blankPageEncoded,
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            navigationDelegate: (NavigationRequest request) {
+              return (request.url.contains('youtube.com'))
+                  ? NavigationDecision.prevent
+                  : NavigationDecision.navigate;
+            },
+            onPageFinished: (String url) => pageLoads.add(url),
+          ),
+        ),
+      );
+
+      await pageLoads.stream.first; // Wait for initial page load.
+      final WebViewController controller = await controllerCompleter.future;
+      await controller
+          .runJavascript('location.href = "https://www.youtube.com/"');
+
+      // There should never be any second page load, since our new URL is
+      // blocked. Still wait for a potential page change for some time in order
+      // to give the test a chance to fail.
+      await pageLoads.stream.first
+          .timeout(const Duration(milliseconds: 500), onTimeout: () => '');
+      final String? currentUrl = await controller.currentUrl();
+      expect(currentUrl, isNot(contains('youtube.com')));
+    });
+
+    testWidgets('supports asynchronous decisions', (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      final StreamController<String> pageLoads =
+          StreamController<String>.broadcast();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: blankPageEncoded,
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            navigationDelegate: (NavigationRequest request) async {
+              NavigationDecision decision = NavigationDecision.prevent;
+              decision = await Future<NavigationDecision>.delayed(
+                  const Duration(milliseconds: 10),
+                  () => NavigationDecision.navigate);
+              return decision;
+            },
+            onPageFinished: (String url) => pageLoads.add(url),
+          ),
+        ),
+      );
+
+      await pageLoads.stream.first; // Wait for initial page load.
+      final WebViewController controller = await controllerCompleter.future;
+      await controller.runJavascript('location.href = "$secondaryUrl"');
+
+      await pageLoads.stream.first; // Wait for second page to load.
+      final String? currentUrl = await controller.currentUrl();
+      expect(currentUrl, secondaryUrl);
+    });
+  });
+
+  testWidgets('launches with gestureNavigationEnabled on iOS',
+      (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: SizedBox(
+          width: 400,
+          height: 300,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: primaryUrl,
+            gestureNavigationEnabled: true,
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+          ),
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    final String? currentUrl = await controller.currentUrl();
+    expect(currentUrl, primaryUrl);
+  });
+
+  testWidgets('target _blank opens in same window',
+      (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    final Completer<void> pageLoaded = Completer<void>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          javascriptMode: JavascriptMode.unrestricted,
+          onPageFinished: (String url) {
+            pageLoaded.complete(null);
+          },
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    await controller.runJavascript('window.open("$primaryUrl", "_blank")');
+    await pageLoaded.future;
+    final String? currentUrl = await controller.currentUrl();
+    expect(currentUrl, primaryUrl);
+  });
+
+  testWidgets(
+    'can open new window and go back',
+    (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+      Completer<void> pageLoaded = Completer<void>();
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (String url) {
+              pageLoaded.complete();
+            },
+            initialUrl: primaryUrl,
+          ),
+        ),
+      );
+      final WebViewController controller = await controllerCompleter.future;
+      expect(controller.currentUrl(), completion(primaryUrl));
+      await pageLoaded.future;
+      pageLoaded = Completer<void>();
+
+      await controller.runJavascript('window.open("$secondaryUrl")');
+      await pageLoaded.future;
+      pageLoaded = Completer<void>();
+      expect(controller.currentUrl(), completion(secondaryUrl));
+
+      expect(controller.canGoBack(), completion(true));
+      await controller.goBack();
+      await pageLoaded.future;
+      await expectLater(controller.currentUrl(), completion(primaryUrl));
+    },
+  );
+
+  testWidgets(
+    'clearCache should clear local storage',
+    (WidgetTester tester) async {
+      final Completer<WebViewController> controllerCompleter =
+          Completer<WebViewController>();
+
+      Completer<void> pageLoadCompleter = Completer<void>();
+
+      await tester.pumpWidget(
+        Directionality(
+          textDirection: TextDirection.ltr,
+          child: WebView(
+            key: GlobalKey(),
+            initialUrl: primaryUrl,
+            javascriptMode: JavascriptMode.unrestricted,
+            onPageFinished: (_) => pageLoadCompleter.complete(),
+            onWebViewCreated: (WebViewController controller) {
+              controllerCompleter.complete(controller);
+            },
+          ),
+        ),
+      );
+
+      await pageLoadCompleter.future;
+      pageLoadCompleter = Completer<void>();
+
+      final WebViewController controller = await controllerCompleter.future;
+      await controller.runJavascript('localStorage.setItem("myCat", "Tom");');
+      final String myCatItem = await controller.runJavascriptReturningResult(
+        'localStorage.getItem("myCat");',
+      );
+      expect(myCatItem, _webviewString('Tom'));
+
+      await controller.clearCache();
+      await pageLoadCompleter.future;
+
+      late final String? nullItem;
+      try {
+        nullItem = await controller.runJavascriptReturningResult(
+          'localStorage.getItem("myCat");',
+        );
+      } catch (exception) {
+        if (defaultTargetPlatform == TargetPlatform.iOS &&
+            exception is ArgumentError &&
+            (exception.message as String).contains(
+                'Result of JavaScript execution returned a `null` value.')) {
+          nullItem = '<null>';
+        }
+      }
+      expect(nullItem, _webviewNull());
+    },
+  );
+}
+
+// JavaScript booleans evaluate to different string values on Android and iOS.
+// This utility method returns the string boolean value of the current platform.
+String _webviewBool(bool value) {
+  if (defaultTargetPlatform == TargetPlatform.iOS) {
+    return value ? '1' : '0';
+  }
+  return value ? 'true' : 'false';
+}
+
+// JavaScript `null` evaluate to different string values on Android and iOS.
+// This utility method returns the string boolean value of the current platform.
+String _webviewNull() {
+  if (defaultTargetPlatform == TargetPlatform.iOS) {
+    return '<null>';
+  }
+  return 'null';
+}
+
+// JavaScript String evaluate to different string values on Android and iOS.
+// This utility method returns the string boolean value of the current platform.
+String _webviewString(String value) {
+  if (defaultTargetPlatform == TargetPlatform.iOS) {
+    return value;
+  }
+  return '"$value"';
+}
+
+/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests.
+Future<String> _getUserAgent(WebViewController controller) async {
+  return _runJavascriptReturningResult(controller, 'navigator.userAgent;');
+}
+
+Future<String> _runJavascriptReturningResult(
+    WebViewController controller, String js) async {
+  if (defaultTargetPlatform == TargetPlatform.iOS) {
+    return controller.runJavascriptReturningResult(js);
+  }
+  return jsonDecode(await controller.runJavascriptReturningResult(js))
+      as String;
+}
+
+class ResizableWebView extends StatefulWidget {
+  const ResizableWebView({
+    super.key,
+    required this.onResize,
+    required this.onPageFinished,
+  });
+
+  final JavascriptMessageHandler onResize;
+  final VoidCallback onPageFinished;
+
+  @override
+  State<StatefulWidget> createState() => ResizableWebViewState();
+}
+
+class ResizableWebViewState extends State<ResizableWebView> {
+  double webViewWidth = 200;
+  double webViewHeight = 200;
+
+  static const String resizePage = '''
+        <!DOCTYPE html><html>
+        <head><title>Resize test</title>
+          <script type="text/javascript">
+            function onResize() {
+              Resize.postMessage("resize");
+            }
+            function onLoad() {
+              window.onresize = onResize;
+            }
+          </script>
+        </head>
+        <body onload="onLoad();" bgColor="blue">
+        </body>
+        </html>
+      ''';
+
+  @override
+  Widget build(BuildContext context) {
+    final String resizeTestBase64 =
+        base64Encode(const Utf8Encoder().convert(resizePage));
+    return Directionality(
+      textDirection: TextDirection.ltr,
+      child: Column(
+        children: <Widget>[
+          SizedBox(
+            width: webViewWidth,
+            height: webViewHeight,
+            child: WebView(
+              initialUrl:
+                  'data:text/html;charset=utf-8;base64,$resizeTestBase64',
+              javascriptChannels: <JavascriptChannel>{
+                JavascriptChannel(
+                  name: 'Resize',
+                  onMessageReceived: widget.onResize,
+                ),
+              },
+              onPageFinished: (_) => widget.onPageFinished(),
+              javascriptMode: JavascriptMode.unrestricted,
+            ),
+          ),
+          TextButton(
+            key: const Key('resizeButton'),
+            onPressed: () {
+              setState(() {
+                webViewWidth += 100.0;
+                webViewHeight += 100.0;
+              });
+            },
+            child: const Text('ResizeButton'),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart
index 8dd8321..7763327 100644
--- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart
+++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart
@@ -19,6 +19,8 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:integration_test/integration_test.dart';
 import 'package:webview_flutter/webview_flutter.dart';
+import 'package:webview_flutter_android/webview_flutter_android.dart';
+import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
 
 Future<void> main() async {
   IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -43,163 +45,93 @@
   final String secondaryUrl = '$prefixUrl/secondary.txt';
   final String headersUrl = '$prefixUrl/headers';
 
-  testWidgets('initialUrl', (WidgetTester tester) async {
-    final Completer<WebViewController> controllerCompleter =
-        Completer<WebViewController>();
-    final Completer<void> pageFinishedCompleter = Completer<void>();
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: GlobalKey(),
-          initialUrl: primaryUrl,
-          onWebViewCreated: (WebViewController controller) {
-            controllerCompleter.complete(controller);
-          },
-          onPageFinished: pageFinishedCompleter.complete,
-        ),
-      ),
-    );
+  testWidgets('loadRequest', (WidgetTester tester) async {
+    final Completer<void> pageFinished = Completer<void>();
 
-    final WebViewController controller = await controllerCompleter.future;
-    await pageFinishedCompleter.future;
+    final WebViewController controller = WebViewController()
+      ..setNavigationDelegate(
+        NavigationDelegate(onPageFinished: (_) => pageFinished.complete()),
+      )
+      ..loadRequest(Uri.parse(primaryUrl));
+
+    await tester.pumpWidget(WebViewWidget(controller: controller));
+
+    await pageFinished.future;
 
     final String? currentUrl = await controller.currentUrl();
     expect(currentUrl, primaryUrl);
   });
 
-  testWidgets('loadUrl', (WidgetTester tester) async {
-    final Completer<WebViewController> controllerCompleter =
-        Completer<WebViewController>();
-    final StreamController<String> pageLoads = StreamController<String>();
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: GlobalKey(),
-          initialUrl: primaryUrl,
-          onWebViewCreated: (WebViewController controller) {
-            controllerCompleter.complete(controller);
-          },
-          onPageFinished: (String url) {
-            pageLoads.add(url);
-          },
-        ),
-      ),
-    );
-    final WebViewController controller = await controllerCompleter.future;
+  testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async {
+    final Completer<void> pageFinished = Completer<void>();
 
-    await controller.loadUrl(secondaryUrl);
+    final WebViewController controller = WebViewController()
+      ..setJavaScriptMode(JavaScriptMode.unrestricted)
+      ..setNavigationDelegate(
+        NavigationDelegate(onPageFinished: (_) => pageFinished.complete()),
+      )
+      ..loadRequest(Uri.parse(primaryUrl));
+
+    await tester.pumpWidget(WebViewWidget(controller: controller));
+
+    await pageFinished.future;
+
     await expectLater(
-      pageLoads.stream.firstWhere((String url) => url == secondaryUrl),
-      completion(secondaryUrl),
+      controller.runJavaScriptReturningResult('1 + 1'),
+      completion(2),
     );
   });
 
-  testWidgets('evaluateJavascript', (WidgetTester tester) async {
-    final Completer<WebViewController> controllerCompleter =
-        Completer<WebViewController>();
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: GlobalKey(),
-          initialUrl: primaryUrl,
-          onWebViewCreated: (WebViewController controller) {
-            controllerCompleter.complete(controller);
-          },
-          javascriptMode: JavascriptMode.unrestricted,
-        ),
-      ),
-    );
-    final WebViewController controller = await controllerCompleter.future;
-    // ignore: deprecated_member_use
-    final String result = await controller.evaluateJavascript('1 + 1');
-    expect(result, equals('2'));
-  });
-
-  testWidgets('loadUrl with headers', (WidgetTester tester) async {
-    final Completer<WebViewController> controllerCompleter =
-        Completer<WebViewController>();
-    final StreamController<String> pageStarts = StreamController<String>();
-    final StreamController<String> pageLoads = StreamController<String>();
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: GlobalKey(),
-          initialUrl: primaryUrl,
-          onWebViewCreated: (WebViewController controller) {
-            controllerCompleter.complete(controller);
-          },
-          javascriptMode: JavascriptMode.unrestricted,
-          onPageStarted: (String url) {
-            pageStarts.add(url);
-          },
-          onPageFinished: (String url) {
-            pageLoads.add(url);
-          },
-        ),
-      ),
-    );
-    final WebViewController controller = await controllerCompleter.future;
+  testWidgets('loadRequest with headers', (WidgetTester tester) async {
     final Map<String, String> headers = <String, String>{
       'test_header': 'flutter_test_header'
     };
-    await controller.loadUrl(headersUrl, headers: headers);
-    final String? currentUrl = await controller.currentUrl();
-    expect(currentUrl, headersUrl);
 
-    await pageStarts.stream.firstWhere((String url) => url == currentUrl);
-    await pageLoads.stream.firstWhere((String url) => url == currentUrl);
+    final StreamController<String> pageLoads = StreamController<String>();
 
-    final String content = await controller
-        .runJavascriptReturningResult('document.documentElement.innerText');
+    final WebViewController controller = WebViewController()
+      ..setJavaScriptMode(JavaScriptMode.unrestricted)
+      ..setNavigationDelegate(
+        NavigationDelegate(onPageFinished: (String url) => pageLoads.add(url)),
+      );
+
+    await tester.pumpWidget(WebViewWidget(controller: controller));
+
+    controller.loadRequest(Uri.parse(headersUrl), headers: headers);
+
+    await pageLoads.stream.firstWhere((String url) => url == headersUrl);
+
+    final String content = await controller.runJavaScriptReturningResult(
+      'document.documentElement.innerText',
+    ) as String;
     expect(content.contains('flutter_test_header'), isTrue);
   });
 
   testWidgets('JavascriptChannel', (WidgetTester tester) async {
-    final Completer<WebViewController> controllerCompleter =
-        Completer<WebViewController>();
-    final Completer<void> pageStarted = Completer<void>();
-    final Completer<void> pageLoaded = Completer<void>();
+    final Completer<void> pageFinished = Completer<void>();
+    final WebViewController controller = WebViewController()
+      ..setJavaScriptMode(JavaScriptMode.unrestricted)
+      ..setNavigationDelegate(
+        NavigationDelegate(onPageFinished: (_) => pageFinished.complete()),
+      );
+
     final Completer<String> channelCompleter = Completer<String>();
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: GlobalKey(),
-          // This is the data URL for: '<!DOCTYPE html>'
-          initialUrl:
-              'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
-          onWebViewCreated: (WebViewController controller) {
-            controllerCompleter.complete(controller);
-          },
-          javascriptMode: JavascriptMode.unrestricted,
-          javascriptChannels: <JavascriptChannel>{
-            JavascriptChannel(
-              name: 'Echo',
-              onMessageReceived: (JavascriptMessage message) {
-                channelCompleter.complete(message.message);
-              },
-            ),
-          },
-          onPageStarted: (String url) {
-            pageStarted.complete(null);
-          },
-          onPageFinished: (String url) {
-            pageLoaded.complete(null);
-          },
-        ),
-      ),
+    await controller.addJavaScriptChannel(
+      'Echo',
+      onMessageReceived: (JavaScriptMessage message) {
+        channelCompleter.complete(message.message);
+      },
     );
-    final WebViewController controller = await controllerCompleter.future;
-    await pageStarted.future;
-    await pageLoaded.future;
 
-    expect(channelCompleter.isCompleted, isFalse);
-    await controller.runJavascript('Echo.postMessage("hello");');
+    await controller.loadHtmlString(
+      'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
+    );
 
+    await tester.pumpWidget(WebViewWidget(controller: controller));
+
+    await pageFinished.future;
+
+    await controller.runJavaScript('Echo.postMessage("hello");');
     await expectLater(channelCompleter.future, completion('hello'));
   });
 
@@ -210,7 +142,7 @@
 
     bool resizeButtonTapped = false;
     await tester.pumpWidget(ResizableWebView(
-      onResize: (_) {
+      onResize: () {
         if (resizeButtonTapped) {
           buttonTapResizeCompleter.complete();
         } else {
@@ -219,6 +151,7 @@
       },
       onPageFinished: () => onPageFinished.complete(),
     ));
+
     await onPageFinished.future;
     // Wait for a potential call to resize after page is loaded.
     await initialResizeCompleter.future.timeout(
@@ -227,98 +160,30 @@
     );
 
     resizeButtonTapped = true;
+
     await tester.tap(find.byKey(const ValueKey<String>('resizeButton')));
     await tester.pumpAndSettle();
-    expect(buttonTapResizeCompleter.future, completes);
+
+    await expectLater(buttonTapResizeCompleter.future, completes);
   });
 
   testWidgets('set custom userAgent', (WidgetTester tester) async {
-    final Completer<WebViewController> controllerCompleter1 =
-        Completer<WebViewController>();
-    final GlobalKey globalKey = GlobalKey();
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: globalKey,
-          initialUrl: 'about:blank',
-          javascriptMode: JavascriptMode.unrestricted,
-          userAgent: 'Custom_User_Agent1',
-          onWebViewCreated: (WebViewController controller) {
-            controllerCompleter1.complete(controller);
-          },
-        ),
-      ),
-    );
-    final WebViewController controller1 = await controllerCompleter1.future;
-    final String customUserAgent1 = await _getUserAgent(controller1);
-    expect(customUserAgent1, 'Custom_User_Agent1');
-    // rebuild the WebView with a different user agent.
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: globalKey,
-          initialUrl: 'about:blank',
-          javascriptMode: JavascriptMode.unrestricted,
-          userAgent: 'Custom_User_Agent2',
-        ),
-      ),
-    );
+    final Completer<void> pageFinished = Completer<void>();
 
-    final String customUserAgent2 = await _getUserAgent(controller1);
-    expect(customUserAgent2, 'Custom_User_Agent2');
-  });
+    final WebViewController controller = WebViewController()
+      ..setJavaScriptMode(JavaScriptMode.unrestricted)
+      ..setNavigationDelegate(NavigationDelegate(
+        onPageFinished: (_) => pageFinished.complete(),
+      ))
+      ..setUserAgent('Custom_User_Agent1')
+      ..loadRequest(Uri.parse('about:blank'));
 
-  testWidgets('use default platform userAgent after webView is rebuilt',
-      (WidgetTester tester) async {
-    final Completer<WebViewController> controllerCompleter =
-        Completer<WebViewController>();
-    final GlobalKey globalKey = GlobalKey();
-    // Build the webView with no user agent to get the default platform user agent.
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: globalKey,
-          initialUrl: primaryUrl,
-          javascriptMode: JavascriptMode.unrestricted,
-          onWebViewCreated: (WebViewController controller) {
-            controllerCompleter.complete(controller);
-          },
-        ),
-      ),
-    );
-    final WebViewController controller = await controllerCompleter.future;
-    final String defaultPlatformUserAgent = await _getUserAgent(controller);
-    // rebuild the WebView with a custom user agent.
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: globalKey,
-          initialUrl: 'about:blank',
-          javascriptMode: JavascriptMode.unrestricted,
-          userAgent: 'Custom_User_Agent',
-        ),
-      ),
-    );
+    await tester.pumpWidget(WebViewWidget(controller: controller));
+
+    await pageFinished.future;
+
     final String customUserAgent = await _getUserAgent(controller);
-    expect(customUserAgent, 'Custom_User_Agent');
-    // rebuilds the WebView with no user agent.
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: globalKey,
-          initialUrl: 'about:blank',
-          javascriptMode: JavascriptMode.unrestricted,
-        ),
-      ),
-    );
-
-    final String customUserAgent2 = await _getUserAgent(controller);
-    expect(customUserAgent2, defaultPlatformUserAgent);
+    expect(customUserAgent, 'Custom_User_Agent1');
   });
 
   group('Video playback policy', () {
@@ -362,219 +227,156 @@
     });
 
     testWidgets('Auto media playback', (WidgetTester tester) async {
-      Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
       Completer<void> pageLoaded = Completer<void>();
 
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
-          ),
-        ),
-      );
-      WebViewController controller = await controllerCompleter.future;
-      await pageLoaded.future;
+      late PlatformWebViewControllerCreationParams params;
+      if (defaultTargetPlatform == TargetPlatform.iOS) {
+        params = WebKitWebViewControllerCreationParams(
+          mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
+        );
+      } else {
+        params = const PlatformWebViewControllerCreationParams();
+      }
 
-      String isPaused =
-          await controller.runJavascriptReturningResult('isPaused();');
-      expect(isPaused, _webviewBool(false));
+      WebViewController controller =
+          WebViewController.fromPlatformCreationParams(params)
+            ..setJavaScriptMode(JavaScriptMode.unrestricted)
+            ..setNavigationDelegate(
+              NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()),
+            );
 
-      controllerCompleter = Completer<WebViewController>();
-      pageLoaded = Completer<void>();
+      if (controller.platform is AndroidWebViewController) {
+        (controller.platform as AndroidWebViewController)
+            .setMediaPlaybackRequiresUserGesture(false);
+      }
 
-      // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-          ),
-        ),
+      await controller.loadRequest(
+        Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64'),
       );
 
-      controller = await controllerCompleter.future;
+      await tester.pumpWidget(WebViewWidget(controller: controller));
+
       await pageLoaded.future;
 
-      isPaused = await controller.runJavascriptReturningResult('isPaused();');
-      expect(isPaused, _webviewBool(true));
-    });
-
-    testWidgets('Changes to initialMediaPlaybackPolicy are ignored',
-        (WidgetTester tester) async {
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
-      Completer<void> pageLoaded = Completer<void>();
-
-      final GlobalKey key = GlobalKey();
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: key,
-            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
-          ),
-        ),
-      );
-      final WebViewController controller = await controllerCompleter.future;
-      await pageLoaded.future;
-
-      String isPaused =
-          await controller.runJavascriptReturningResult('isPaused();');
-      expect(isPaused, _webviewBool(false));
+      bool isPaused =
+          await controller.runJavaScriptReturningResult('isPaused();') as bool;
+      expect(isPaused, false);
 
       pageLoaded = Completer<void>();
+      controller = WebViewController()
+        ..setJavaScriptMode(JavaScriptMode.unrestricted)
+        ..setNavigationDelegate(
+          NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()),
+        )
+        ..loadRequest(
+          Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64'),
+        );
 
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: key,
-            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-          ),
-        ),
-      );
-
-      await controller.reload();
+      await tester.pumpWidget(WebViewWidget(controller: controller));
 
       await pageLoaded.future;
 
-      isPaused = await controller.runJavascriptReturningResult('isPaused();');
-      expect(isPaused, _webviewBool(false));
+      isPaused =
+          await controller.runJavaScriptReturningResult('isPaused();') as bool;
+      expect(isPaused, true);
     });
 
-    testWidgets('Video plays inline when allowsInlineMediaPlayback is true',
-        (WidgetTester tester) async {
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
+    testWidgets('Video plays inline', (WidgetTester tester) async {
       final Completer<void> pageLoaded = Completer<void>();
       final Completer<void> videoPlaying = Completer<void>();
 
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            javascriptChannels: <JavascriptChannel>{
-              JavascriptChannel(
-                name: 'VideoTestTime',
-                onMessageReceived: (JavascriptMessage message) {
-                  final double currentTime = double.parse(message.message);
-                  // Let it play for at least 1 second to make sure the related video's properties are set.
-                  if (currentTime > 1 && !videoPlaying.isCompleted) {
-                    videoPlaying.complete(null);
-                  }
-                },
-              ),
-            },
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
-            allowsInlineMediaPlayback: true,
-          ),
-        ),
-      );
-      final WebViewController controller = await controllerCompleter.future;
-      await pageLoaded.future;
+      late PlatformWebViewControllerCreationParams params;
+      if (defaultTargetPlatform == TargetPlatform.iOS) {
+        params = WebKitWebViewControllerCreationParams(
+          mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
+          allowsInlineMediaPlayback: true,
+        );
+      } else {
+        params = const PlatformWebViewControllerCreationParams();
+      }
+      final WebViewController controller =
+          WebViewController.fromPlatformCreationParams(params)
+            ..setJavaScriptMode(JavaScriptMode.unrestricted)
+            ..setNavigationDelegate(
+              NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()),
+            )
+            ..addJavaScriptChannel(
+              'VideoTestTime',
+              onMessageReceived: (JavaScriptMessage message) {
+                final double currentTime = double.parse(message.message);
+                // Let it play for at least 1 second to make sure the related video's properties are set.
+                if (currentTime > 1 && !videoPlaying.isCompleted) {
+                  videoPlaying.complete(null);
+                }
+              },
+            );
 
-      // Pump once to trigger the video play.
-      await tester.pump();
+      if (controller.platform is AndroidWebViewController) {
+        (controller.platform as AndroidWebViewController)
+            .setMediaPlaybackRequiresUserGesture(false);
+      }
+
+      await controller.loadRequest(
+        Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64'),
+      );
+
+      await tester.pumpWidget(WebViewWidget(controller: controller));
+      await tester.pumpAndSettle();
+
+      await pageLoaded.future;
 
       // Makes sure we get the correct event that indicates the video is actually playing.
       await videoPlaying.future;
 
-      final String fullScreen =
-          await controller.runJavascriptReturningResult('isFullScreen();');
-      expect(fullScreen, _webviewBool(false));
+      final bool fullScreen = await controller
+          .runJavaScriptReturningResult('isFullScreen();') as bool;
+      expect(fullScreen, false);
     });
 
     // allowsInlineMediaPlayback is a noop on Android, so it is skipped.
     testWidgets(
         'Video plays full screen when allowsInlineMediaPlayback is false',
         (WidgetTester tester) async {
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
       final Completer<void> pageLoaded = Completer<void>();
       final Completer<void> videoPlaying = Completer<void>();
 
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            javascriptChannels: <JavascriptChannel>{
-              JavascriptChannel(
-                name: 'VideoTestTime',
-                onMessageReceived: (JavascriptMessage message) {
-                  final double currentTime = double.parse(message.message);
-                  // Let it play for at least 1 second to make sure the related video's properties are set.
-                  if (currentTime > 1 && !videoPlaying.isCompleted) {
-                    videoPlaying.complete(null);
-                  }
-                },
-              ),
-            },
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
-          ),
+      final WebViewController controller =
+          WebViewController.fromPlatformCreationParams(
+        WebKitWebViewControllerCreationParams(
+          mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
         ),
-      );
-      final WebViewController controller = await controllerCompleter.future;
-      await pageLoaded.future;
+      )
+            ..setJavaScriptMode(JavaScriptMode.unrestricted)
+            ..setNavigationDelegate(
+              NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()),
+            )
+            ..addJavaScriptChannel(
+              'VideoTestTime',
+              onMessageReceived: (JavaScriptMessage message) {
+                final double currentTime = double.parse(message.message);
+                // Let it play for at least 1 second to make sure the related video's properties are set.
+                if (currentTime > 1 && !videoPlaying.isCompleted) {
+                  videoPlaying.complete(null);
+                }
+              },
+            )
+            ..loadRequest(
+              Uri.parse(
+                'data:text/html;charset=utf-8;base64,$videoTestBase64',
+              ),
+            );
 
-      // Pump once to trigger the video play.
-      await tester.pump();
+      await tester.pumpWidget(WebViewWidget(controller: controller));
+      await tester.pumpAndSettle();
+
+      await pageLoaded.future;
 
       // Makes sure we get the correct event that indicates the video is actually playing.
       await videoPlaying.future;
 
-      final String fullScreen =
-          await controller.runJavascriptReturningResult('isFullScreen();');
-      expect(fullScreen, _webviewBool(true));
+      final bool fullScreen = await controller
+          .runJavaScriptReturningResult('isFullScreen();') as bool;
+      expect(fullScreen, true);
     }, skip: Platform.isAndroid);
   });
 
@@ -610,138 +412,60 @@
     });
 
     testWidgets('Auto media playback', (WidgetTester tester) async {
-      Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
-      Completer<void> pageStarted = Completer<void>();
       Completer<void> pageLoaded = Completer<void>();
 
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            onPageStarted: (String url) {
-              pageStarted.complete(null);
-            },
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
-          ),
-        ),
+      late PlatformWebViewControllerCreationParams params;
+      if (defaultTargetPlatform == TargetPlatform.iOS) {
+        params = WebKitWebViewControllerCreationParams(
+          mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
+        );
+      } else {
+        params = const PlatformWebViewControllerCreationParams();
+      }
+
+      WebViewController controller =
+          WebViewController.fromPlatformCreationParams(params)
+            ..setJavaScriptMode(JavaScriptMode.unrestricted)
+            ..setNavigationDelegate(
+              NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()),
+            );
+
+      if (controller.platform is AndroidWebViewController) {
+        (controller.platform as AndroidWebViewController)
+            .setMediaPlaybackRequiresUserGesture(false);
+      }
+
+      await controller.loadRequest(
+        Uri.parse('data:text/html;charset=utf-8;base64,$audioTestBase64'),
       );
-      WebViewController controller = await controllerCompleter.future;
-      await pageStarted.future;
+
+      await tester.pumpWidget(WebViewWidget(controller: controller));
+      await tester.pumpAndSettle();
+
       await pageLoaded.future;
 
-      String isPaused =
-          await controller.runJavascriptReturningResult('isPaused();');
-      expect(isPaused, _webviewBool(false));
+      bool isPaused =
+          await controller.runJavaScriptReturningResult('isPaused();') as bool;
+      expect(isPaused, false);
 
-      controllerCompleter = Completer<WebViewController>();
-      pageStarted = Completer<void>();
       pageLoaded = Completer<void>();
+      controller = WebViewController()
+        ..setJavaScriptMode(JavaScriptMode.unrestricted)
+        ..setNavigationDelegate(
+          NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()),
+        )
+        ..loadRequest(
+          Uri.parse('data:text/html;charset=utf-8;base64,$audioTestBase64'),
+        );
 
-      // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            onPageStarted: (String url) {
-              pageStarted.complete(null);
-            },
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-          ),
-        ),
-      );
+      await tester.pumpWidget(WebViewWidget(controller: controller));
+      await tester.pumpAndSettle();
 
-      controller = await controllerCompleter.future;
-      await pageStarted.future;
       await pageLoaded.future;
 
-      isPaused = await controller.runJavascriptReturningResult('isPaused();');
-      expect(isPaused, _webviewBool(true));
-    });
-
-    testWidgets('Changes to initialMediaPlaybackPolicy are ignored',
-        (WidgetTester tester) async {
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
-      Completer<void> pageStarted = Completer<void>();
-      Completer<void> pageLoaded = Completer<void>();
-
-      final GlobalKey key = GlobalKey();
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: key,
-            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            onPageStarted: (String url) {
-              pageStarted.complete(null);
-            },
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
-          ),
-        ),
-      );
-      final WebViewController controller = await controllerCompleter.future;
-      await pageStarted.future;
-      await pageLoaded.future;
-
-      String isPaused =
-          await controller.runJavascriptReturningResult('isPaused();');
-      expect(isPaused, _webviewBool(false));
-
-      pageStarted = Completer<void>();
-      pageLoaded = Completer<void>();
-
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: key,
-            initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            onPageStarted: (String url) {
-              pageStarted.complete(null);
-            },
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-          ),
-        ),
-      );
-
-      await controller.reload();
-
-      await pageStarted.future;
-      await pageLoaded.future;
-
-      isPaused = await controller.runJavascriptReturningResult('isPaused();');
-      expect(isPaused, _webviewBool(false));
+      isPaused =
+          await controller.runJavaScriptReturningResult('isPaused();') as bool;
+      expect(isPaused, true);
     });
   });
 
@@ -756,39 +480,26 @@
       ''';
     final String getTitleTestBase64 =
         base64Encode(const Utf8Encoder().convert(getTitleTest));
-    final Completer<void> pageStarted = Completer<void>();
     final Completer<void> pageLoaded = Completer<void>();
-    final Completer<WebViewController> controllerCompleter =
-        Completer<WebViewController>();
 
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64',
-          javascriptMode: JavascriptMode.unrestricted,
-          onWebViewCreated: (WebViewController controller) {
-            controllerCompleter.complete(controller);
-          },
-          onPageStarted: (String url) {
-            pageStarted.complete(null);
-          },
-          onPageFinished: (String url) {
-            pageLoaded.complete(null);
-          },
-        ),
-      ),
-    );
+    final WebViewController controller = WebViewController()
+      ..setJavaScriptMode(JavaScriptMode.unrestricted)
+      ..setNavigationDelegate(NavigationDelegate(
+        onPageFinished: (_) => pageLoaded.complete(),
+      ))
+      ..loadRequest(
+        Uri.parse('data:text/html;charset=utf-8;base64,$getTitleTestBase64'),
+      );
 
-    final WebViewController controller = await controllerCompleter.future;
-    await pageStarted.future;
+    await tester.pumpWidget(WebViewWidget(controller: controller));
+
     await pageLoaded.future;
 
     // On at least iOS, it does not appear to be guaranteed that the native
     // code has the title when the page load completes. Execute some JavaScript
     // before checking the title to ensure that the page has been fully parsed
     // and processed.
-    await controller.runJavascript('1;');
+    await controller.runJavaScript('1;');
 
     final String? title = await controller.getTitle();
     expect(title, 'Some title');
@@ -821,32 +532,22 @@
           base64Encode(const Utf8Encoder().convert(scrollTestPage));
 
       final Completer<void> pageLoaded = Completer<void>();
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
+      final WebViewController controller = WebViewController()
+        ..setJavaScriptMode(JavaScriptMode.unrestricted)
+        ..setNavigationDelegate(NavigationDelegate(
+          onPageFinished: (_) => pageLoaded.complete(),
+        ))
+        ..loadRequest(Uri.parse(
+          'data:text/html;charset=utf-8;base64,$scrollTestPageBase64',
+        ));
 
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            initialUrl:
-                'data:text/html;charset=utf-8;base64,$scrollTestPageBase64',
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            onPageFinished: (String url) {
-              pageLoaded.complete(null);
-            },
-          ),
-        ),
-      );
+      await tester.pumpWidget(WebViewWidget(controller: controller));
 
-      final WebViewController controller = await controllerCompleter.future;
       await pageLoaded.future;
 
       await tester.pumpAndSettle(const Duration(seconds: 3));
 
-      int scrollPosX = await controller.getScrollX();
-      int scrollPosY = await controller.getScrollY();
+      Offset scrollPos = await controller.getScrollPosition();
 
       // Check scrollTo()
       const int X_SCROLL = 123;
@@ -854,95 +555,51 @@
       // Get the initial position; this ensures that scrollTo is actually
       // changing something, but also gives the native view's scroll position
       // time to settle.
-      expect(scrollPosX, isNot(X_SCROLL));
-      expect(scrollPosX, isNot(Y_SCROLL));
+      expect(scrollPos.dx, isNot(X_SCROLL));
+      expect(scrollPos.dy, isNot(Y_SCROLL));
 
       await controller.scrollTo(X_SCROLL, Y_SCROLL);
-      scrollPosX = await controller.getScrollX();
-      scrollPosY = await controller.getScrollY();
-      expect(scrollPosX, X_SCROLL);
-      expect(scrollPosY, Y_SCROLL);
+      scrollPos = await controller.getScrollPosition();
+      expect(scrollPos.dx, X_SCROLL);
+      expect(scrollPos.dy, Y_SCROLL);
 
       // Check scrollBy() (on top of scrollTo())
       await controller.scrollBy(X_SCROLL, Y_SCROLL);
-      scrollPosX = await controller.getScrollX();
-      scrollPosY = await controller.getScrollY();
-      expect(scrollPosX, X_SCROLL * 2);
-      expect(scrollPosY, Y_SCROLL * 2);
+      scrollPos = await controller.getScrollPosition();
+      expect(scrollPos.dx, X_SCROLL * 2);
+      expect(scrollPos.dy, Y_SCROLL * 2);
     });
   });
 
-  // Minimal end-to-end testing of the legacy Android implementation.
-  group('AndroidWebView (virtual display)', () {
-    setUpAll(() {
-      WebView.platform = AndroidWebView();
-    });
-
-    tearDownAll(() {
-      WebView.platform = null;
-    });
-
-    testWidgets('initialUrl', (WidgetTester tester) async {
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
-      final Completer<void> pageFinishedCompleter = Completer<void>();
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: primaryUrl,
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            onPageFinished: pageFinishedCompleter.complete,
-          ),
-        ),
-      );
-
-      final WebViewController controller = await controllerCompleter.future;
-      await pageFinishedCompleter.future;
-
-      final String? currentUrl = await controller.currentUrl();
-      expect(currentUrl, primaryUrl);
-    });
-  }, skip: !Platform.isAndroid);
-
   group('NavigationDelegate', () {
     const String blankPage = '<!DOCTYPE html><head></head><body></body></html>';
     final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,'
         '${base64Encode(const Utf8Encoder().convert(blankPage))}';
 
     testWidgets('can allow requests', (WidgetTester tester) async {
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
-      final StreamController<String> pageLoads =
-          StreamController<String>.broadcast();
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: blankPageEncoded,
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            navigationDelegate: (NavigationRequest request) {
-              return (request.url.contains('youtube.com'))
-                  ? NavigationDecision.prevent
-                  : NavigationDecision.navigate;
-            },
-            onPageFinished: (String url) => pageLoads.add(url),
-          ),
-        ),
-      );
+      Completer<void> pageLoaded = Completer<void>();
 
-      await pageLoads.stream.first; // Wait for initial page load.
-      final WebViewController controller = await controllerCompleter.future;
-      await controller.runJavascript('location.href = "$secondaryUrl"');
+      final WebViewController controller = WebViewController()
+        ..setJavaScriptMode(JavaScriptMode.unrestricted)
+        ..setNavigationDelegate(NavigationDelegate(
+          onPageFinished: (_) => pageLoaded.complete(),
+          onNavigationRequest: (NavigationRequest navigationRequest) {
+            return (navigationRequest.url.contains('youtube.com'))
+                ? NavigationDecision.prevent
+                : NavigationDecision.navigate;
+          },
+        ));
 
-      await pageLoads.stream.first; // Wait for the next page load.
+      await tester.pumpWidget(WebViewWidget(controller: controller));
+
+      controller.loadRequest(Uri.parse(blankPageEncoded));
+
+      await pageLoaded.future; // Wait for initial page load.
+
+      pageLoaded = Completer<void>();
+      await controller.runJavaScript('location.href = "$secondaryUrl"');
+      await pageLoaded.future; // Wait for the next page load.
+
       final String? currentUrl = await controller.currentUrl();
       expect(currentUrl, secondaryUrl);
     });
@@ -951,30 +608,18 @@
       final Completer<WebResourceError> errorCompleter =
           Completer<WebResourceError>();
 
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: 'https://www.notawebsite..com',
-            onWebResourceError: (WebResourceError error) {
-              errorCompleter.complete(error);
-            },
-          ),
-        ),
-      );
+      final WebViewController controller = WebViewController()
+        ..setJavaScriptMode(JavaScriptMode.unrestricted)
+        ..setNavigationDelegate(
+            NavigationDelegate(onWebResourceError: (WebResourceError error) {
+          errorCompleter.complete(error);
+        }))
+        ..loadRequest(Uri.parse('https://www.notawebsite..com'));
+
+      await tester.pumpWidget(WebViewWidget(controller: controller));
 
       final WebResourceError error = await errorCompleter.future;
       expect(error, isNotNull);
-
-      if (Platform.isIOS) {
-        expect(error.domain, isNotNull);
-        expect(error.failingUrl, isNull);
-      } else if (Platform.isAndroid) {
-        expect(error.errorType, isNotNull);
-        expect(error.failingUrl?.startsWith('https://www.notawebsite..com'),
-            isTrue);
-      }
     });
 
     testWidgets('onWebResourceError is not called with valid url',
@@ -983,190 +628,99 @@
           Completer<WebResourceError>();
       final Completer<void> pageFinishCompleter = Completer<void>();
 
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl:
-                'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
-            onWebResourceError: (WebResourceError error) {
-              errorCompleter.complete(error);
-            },
-            onPageFinished: (_) => pageFinishCompleter.complete(),
-          ),
-        ),
-      );
+      final WebViewController controller = WebViewController()
+        ..setJavaScriptMode(JavaScriptMode.unrestricted)
+        ..setNavigationDelegate(NavigationDelegate(
+          onPageFinished: (_) => pageFinishCompleter.complete(),
+          onWebResourceError: (WebResourceError error) {
+            errorCompleter.complete(error);
+          },
+        ))
+        ..loadRequest(
+          Uri.parse('data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+'),
+        );
+
+      await tester.pumpWidget(WebViewWidget(controller: controller));
 
       expect(errorCompleter.future, doesNotComplete);
       await pageFinishCompleter.future;
     });
 
-    testWidgets(
-      'onWebResourceError only called for main frame',
-      (WidgetTester tester) async {
-        const String iframeTest = '''
-        <!DOCTYPE html>
-        <html>
-        <head>
-          <title>WebResourceError test</title>
-        </head>
-        <body>
-          <iframe src="https://notawebsite..com"></iframe>
-        </body>
-        </html>
-       ''';
-        final String iframeTestBase64 =
-            base64Encode(const Utf8Encoder().convert(iframeTest));
-
-        final Completer<WebResourceError> errorCompleter =
-            Completer<WebResourceError>();
-        final Completer<void> pageFinishCompleter = Completer<void>();
-
-        await tester.pumpWidget(
-          Directionality(
-            textDirection: TextDirection.ltr,
-            child: WebView(
-              key: GlobalKey(),
-              initialUrl:
-                  'data:text/html;charset=utf-8;base64,$iframeTestBase64',
-              onWebResourceError: (WebResourceError error) {
-                errorCompleter.complete(error);
-              },
-              onPageFinished: (_) => pageFinishCompleter.complete(),
-            ),
-          ),
-        );
-
-        expect(errorCompleter.future, doesNotComplete);
-        await pageFinishCompleter.future;
-      },
-    );
-
     testWidgets('can block requests', (WidgetTester tester) async {
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
-      final StreamController<String> pageLoads =
-          StreamController<String>.broadcast();
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: blankPageEncoded,
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            navigationDelegate: (NavigationRequest request) {
-              return (request.url.contains('youtube.com'))
+      Completer<void> pageLoaded = Completer<void>();
+
+      final WebViewController controller = WebViewController()
+        ..setJavaScriptMode(JavaScriptMode.unrestricted)
+        ..setNavigationDelegate(NavigationDelegate(
+            onPageFinished: (_) => pageLoaded.complete(),
+            onNavigationRequest: (NavigationRequest navigationRequest) {
+              return (navigationRequest.url.contains('youtube.com'))
                   ? NavigationDecision.prevent
                   : NavigationDecision.navigate;
-            },
-            onPageFinished: (String url) => pageLoads.add(url),
-          ),
-        ),
-      );
+            }));
 
-      await pageLoads.stream.first; // Wait for initial page load.
-      final WebViewController controller = await controllerCompleter.future;
+      await tester.pumpWidget(WebViewWidget(controller: controller));
+
+      controller.loadRequest(Uri.parse(blankPageEncoded));
+
+      await pageLoaded.future; // Wait for initial page load.
+
+      pageLoaded = Completer<void>();
       await controller
-          .runJavascript('location.href = "https://www.youtube.com/"');
+          .runJavaScript('location.href = "https://www.youtube.com/"');
 
       // There should never be any second page load, since our new URL is
       // blocked. Still wait for a potential page change for some time in order
       // to give the test a chance to fail.
-      await pageLoads.stream.first
+      await pageLoaded.future
           .timeout(const Duration(milliseconds: 500), onTimeout: () => '');
       final String? currentUrl = await controller.currentUrl();
       expect(currentUrl, isNot(contains('youtube.com')));
     });
 
     testWidgets('supports asynchronous decisions', (WidgetTester tester) async {
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
-      final StreamController<String> pageLoads =
-          StreamController<String>.broadcast();
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: blankPageEncoded,
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            navigationDelegate: (NavigationRequest request) async {
+      Completer<void> pageLoaded = Completer<void>();
+
+      final WebViewController controller = WebViewController()
+        ..setJavaScriptMode(JavaScriptMode.unrestricted)
+        ..setNavigationDelegate(NavigationDelegate(
+            onPageFinished: (_) => pageLoaded.complete(),
+            onNavigationRequest: (NavigationRequest navigationRequest) async {
               NavigationDecision decision = NavigationDecision.prevent;
               decision = await Future<NavigationDecision>.delayed(
                   const Duration(milliseconds: 10),
                   () => NavigationDecision.navigate);
               return decision;
-            },
-            onPageFinished: (String url) => pageLoads.add(url),
-          ),
-        ),
-      );
+            }));
 
-      await pageLoads.stream.first; // Wait for initial page load.
-      final WebViewController controller = await controllerCompleter.future;
-      await controller.runJavascript('location.href = "$secondaryUrl"');
+      await tester.pumpWidget(WebViewWidget(controller: controller));
 
-      await pageLoads.stream.first; // Wait for second page to load.
+      controller.loadRequest(Uri.parse(blankPageEncoded));
+
+      await pageLoaded.future; // Wait for initial page load.
+
+      pageLoaded = Completer<void>();
+      await controller.runJavaScript('location.href = "$secondaryUrl"');
+      await pageLoaded.future; // Wait for second page to load.
+
       final String? currentUrl = await controller.currentUrl();
       expect(currentUrl, secondaryUrl);
     });
   });
 
-  testWidgets('launches with gestureNavigationEnabled on iOS',
-      (WidgetTester tester) async {
-    final Completer<WebViewController> controllerCompleter =
-        Completer<WebViewController>();
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: SizedBox(
-          width: 400,
-          height: 300,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: primaryUrl,
-            gestureNavigationEnabled: true,
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-          ),
-        ),
-      ),
-    );
-    final WebViewController controller = await controllerCompleter.future;
-    final String? currentUrl = await controller.currentUrl();
-    expect(currentUrl, primaryUrl);
-  });
-
   testWidgets('target _blank opens in same window',
       (WidgetTester tester) async {
-    final Completer<WebViewController> controllerCompleter =
-        Completer<WebViewController>();
     final Completer<void> pageLoaded = Completer<void>();
-    await tester.pumpWidget(
-      Directionality(
-        textDirection: TextDirection.ltr,
-        child: WebView(
-          key: GlobalKey(),
-          onWebViewCreated: (WebViewController controller) {
-            controllerCompleter.complete(controller);
-          },
-          javascriptMode: JavascriptMode.unrestricted,
-          onPageFinished: (String url) {
-            pageLoaded.complete(null);
-          },
-        ),
-      ),
-    );
-    final WebViewController controller = await controllerCompleter.future;
-    await controller.runJavascript('window.open("$primaryUrl", "_blank")');
+
+    final WebViewController controller = WebViewController()
+      ..setJavaScriptMode(JavaScriptMode.unrestricted)
+      ..setNavigationDelegate(NavigationDelegate(
+        onPageFinished: (_) => pageLoaded.complete(),
+      ));
+
+    await tester.pumpWidget(WebViewWidget(controller: controller));
+
+    await controller.runJavaScript('window.open("$primaryUrl", "_blank")');
     await pageLoaded.future;
     final String? currentUrl = await controller.currentUrl();
     expect(currentUrl, primaryUrl);
@@ -1175,31 +729,22 @@
   testWidgets(
     'can open new window and go back',
     (WidgetTester tester) async {
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
       Completer<void> pageLoaded = Completer<void>();
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-            javascriptMode: JavascriptMode.unrestricted,
-            onPageFinished: (String url) {
-              pageLoaded.complete();
-            },
-            initialUrl: primaryUrl,
-          ),
-        ),
-      );
-      final WebViewController controller = await controllerCompleter.future;
+
+      final WebViewController controller = WebViewController()
+        ..setJavaScriptMode(JavaScriptMode.unrestricted)
+        ..setNavigationDelegate(NavigationDelegate(
+          onPageFinished: (_) => pageLoaded.complete(),
+        ))
+        ..loadRequest(Uri.parse(primaryUrl));
+
+      await tester.pumpWidget(WebViewWidget(controller: controller));
+
       expect(controller.currentUrl(), completion(primaryUrl));
       await pageLoaded.future;
       pageLoaded = Completer<void>();
 
-      await controller.runJavascript('window.open("$secondaryUrl")');
+      await controller.runJavaScript('window.open("$secondaryUrl")');
       await pageLoaded.future;
       pageLoaded = Completer<void>();
       expect(controller.currentUrl(), completion(secondaryUrl));
@@ -1212,46 +757,39 @@
   );
 
   testWidgets(
-    'clearCache should clear local storage',
+    'clearLocalStorage',
     (WidgetTester tester) async {
-      final Completer<WebViewController> controllerCompleter =
-          Completer<WebViewController>();
-
       Completer<void> pageLoadCompleter = Completer<void>();
 
-      await tester.pumpWidget(
-        Directionality(
-          textDirection: TextDirection.ltr,
-          child: WebView(
-            key: GlobalKey(),
-            initialUrl: primaryUrl,
-            javascriptMode: JavascriptMode.unrestricted,
-            onPageFinished: (_) => pageLoadCompleter.complete(),
-            onWebViewCreated: (WebViewController controller) {
-              controllerCompleter.complete(controller);
-            },
-          ),
-        ),
-      );
+      final WebViewController controller = WebViewController()
+        ..setJavaScriptMode(JavaScriptMode.unrestricted)
+        ..setNavigationDelegate(NavigationDelegate(
+          onPageFinished: (_) => pageLoadCompleter.complete(),
+        ))
+        ..loadRequest(Uri.parse(primaryUrl));
+
+      await tester.pumpWidget(WebViewWidget(controller: controller));
 
       await pageLoadCompleter.future;
       pageLoadCompleter = Completer<void>();
 
-      final WebViewController controller = await controllerCompleter.future;
-      await controller.runJavascript('localStorage.setItem("myCat", "Tom");');
-      final String myCatItem = await controller.runJavascriptReturningResult(
+      await controller.runJavaScript('localStorage.setItem("myCat", "Tom");');
+      final String myCatItem = await controller.runJavaScriptReturningResult(
         'localStorage.getItem("myCat");',
-      );
-      expect(myCatItem, _webviewString('Tom'));
+      ) as String;
+      expect(myCatItem, _webViewString('Tom'));
 
-      await controller.clearCache();
+      await controller.clearLocalStorage();
+
+      // Reload page to have changes take effect.
+      await controller.reload();
       await pageLoadCompleter.future;
 
       late final String? nullItem;
       try {
-        nullItem = await controller.runJavascriptReturningResult(
+        nullItem = await controller.runJavaScriptReturningResult(
           'localStorage.getItem("myCat");',
-        );
+        ) as String;
       } catch (exception) {
         if (defaultTargetPlatform == TargetPlatform.iOS &&
             exception is ArgumentError &&
@@ -1260,23 +798,14 @@
           nullItem = '<null>';
         }
       }
-      expect(nullItem, _webviewNull());
+      expect(nullItem, _webViewNull());
     },
   );
 }
 
-// JavaScript booleans evaluate to different string values on Android and iOS.
-// This utility method returns the string boolean value of the current platform.
-String _webviewBool(bool value) {
-  if (defaultTargetPlatform == TargetPlatform.iOS) {
-    return value ? '1' : '0';
-  }
-  return value ? 'true' : 'false';
-}
-
 // JavaScript `null` evaluate to different string values on Android and iOS.
 // This utility method returns the string boolean value of the current platform.
-String _webviewNull() {
+String _webViewNull() {
   if (defaultTargetPlatform == TargetPlatform.iOS) {
     return '<null>';
   }
@@ -1285,7 +814,7 @@
 
 // JavaScript String evaluate to different string values on Android and iOS.
 // This utility method returns the string boolean value of the current platform.
-String _webviewString(String value) {
+String _webViewString(String value) {
   if (defaultTargetPlatform == TargetPlatform.iOS) {
     return value;
   }
@@ -1298,20 +827,24 @@
 }
 
 Future<String> _runJavascriptReturningResult(
-    WebViewController controller, String js) async {
+  WebViewController controller,
+  String js,
+) async {
   if (defaultTargetPlatform == TargetPlatform.iOS) {
-    return controller.runJavascriptReturningResult(js);
+    return await controller.runJavaScriptReturningResult(js) as String;
   }
-  return jsonDecode(await controller.runJavascriptReturningResult(js))
+  return jsonDecode(await controller.runJavaScriptReturningResult(js) as String)
       as String;
 }
 
 class ResizableWebView extends StatefulWidget {
-  const ResizableWebView(
-      {Key? key, required this.onResize, required this.onPageFinished})
-      : super(key: key);
+  const ResizableWebView({
+    super.key,
+    required this.onResize,
+    required this.onPageFinished,
+  });
 
-  final JavascriptMessageHandler onResize;
+  final VoidCallback onResize;
   final VoidCallback onPageFinished;
 
   @override
@@ -1319,6 +852,23 @@
 }
 
 class ResizableWebViewState extends State<ResizableWebView> {
+  late final WebViewController controller = WebViewController()
+    ..setJavaScriptMode(JavaScriptMode.unrestricted)
+    ..setNavigationDelegate(NavigationDelegate(
+      onPageFinished: (_) => widget.onPageFinished(),
+    ))
+    ..addJavaScriptChannel(
+      'Resize',
+      onMessageReceived: (_) {
+        widget.onResize();
+      },
+    )
+    ..loadRequest(
+      Uri.parse(
+        'data:text/html;charset=utf-8;base64,${base64Encode(const Utf8Encoder().convert(resizePage))}',
+      ),
+    );
+
   double webViewWidth = 200;
   double webViewHeight = 200;
 
@@ -1341,28 +891,14 @@
 
   @override
   Widget build(BuildContext context) {
-    final String resizeTestBase64 =
-        base64Encode(const Utf8Encoder().convert(resizePage));
     return Directionality(
       textDirection: TextDirection.ltr,
       child: Column(
         children: <Widget>[
           SizedBox(
-            width: webViewWidth,
-            height: webViewHeight,
-            child: WebView(
-              initialUrl:
-                  'data:text/html;charset=utf-8;base64,$resizeTestBase64',
-              javascriptChannels: <JavascriptChannel>{
-                JavascriptChannel(
-                  name: 'Resize',
-                  onMessageReceived: widget.onResize,
-                ),
-              },
-              onPageFinished: (_) => widget.onPageFinished(),
-              javascriptMode: JavascriptMode.unrestricted,
-            ),
-          ),
+              width: webViewWidth,
+              height: webViewHeight,
+              child: WebViewWidget(controller: controller)),
           TextButton(
             key: const Key('resizeButton'),
             onPressed: () {
diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart
index 0e71290..239b417 100644
--- a/packages/webview_flutter/webview_flutter/example/lib/main.dart
+++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// ignore_for_file: public_member_api_docs, avoid_print
+// ignore_for_file: public_member_api_docs
 
 import 'dart:async';
 import 'dart:convert';
@@ -12,6 +12,12 @@
 import 'package:flutter/material.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:webview_flutter/webview_flutter.dart';
+// #docregion platform_imports
+// Import for Android features.
+import 'package:webview_flutter_android/webview_flutter_android.dart';
+// Import for iOS features.
+import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
+// #enddocregion platform_imports
 
 void main() => runApp(const MaterialApp(home: WebViewExample()));
 
@@ -71,24 +77,86 @@
 ''';
 
 class WebViewExample extends StatefulWidget {
-  const WebViewExample({Key? key, this.cookieManager}) : super(key: key);
-
-  final CookieManager? cookieManager;
+  const WebViewExample({super.key});
 
   @override
   State<WebViewExample> createState() => _WebViewExampleState();
 }
 
 class _WebViewExampleState extends State<WebViewExample> {
-  final Completer<WebViewController> _controller =
-      Completer<WebViewController>();
+  late final WebViewController _controller;
 
   @override
   void initState() {
     super.initState();
-    if (Platform.isAndroid) {
-      WebView.platform = SurfaceAndroidWebView();
+
+    // #docregion platform_features
+    late final PlatformWebViewControllerCreationParams params;
+    if (WebViewPlatform.instance is WebKitWebViewPlatform) {
+      params = WebKitWebViewControllerCreationParams(
+        allowsInlineMediaPlayback: true,
+        mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
+      );
+    } else {
+      params = const PlatformWebViewControllerCreationParams();
     }
+
+    final WebViewController controller =
+        WebViewController.fromPlatformCreationParams(params);
+    // #enddocregion platform_features
+
+    controller
+      ..setJavaScriptMode(JavaScriptMode.unrestricted)
+      ..setBackgroundColor(const Color(0x00000000))
+      ..setNavigationDelegate(
+        NavigationDelegate(
+          onProgress: (int progress) {
+            debugPrint('WebView is loading (progress : $progress%)');
+          },
+          onPageStarted: (String url) {
+            debugPrint('Page started loading: $url');
+          },
+          onPageFinished: (String url) {
+            debugPrint('Page finished loading: $url');
+          },
+          onWebResourceError: (WebResourceError error) {
+            debugPrint('''
+Page resource error:
+  code: ${error.errorCode}
+  description: ${error.description}
+  errorType: ${error.errorType}
+  isForMainFrame: ${error.isForMainFrame}
+          ''');
+          },
+          onNavigationRequest: (NavigationRequest request) {
+            if (request.url.startsWith('https://www.youtube.com/')) {
+              debugPrint('blocking navigation to ${request.url}');
+              return NavigationDecision.prevent;
+            }
+            debugPrint('allowing navigation to ${request.url}');
+            return NavigationDecision.navigate;
+          },
+        ),
+      )
+      ..addJavaScriptChannel(
+        'Toaster',
+        onMessageReceived: (JavaScriptMessage message) {
+          ScaffoldMessenger.of(context).showSnackBar(
+            SnackBar(content: Text(message.message)),
+          );
+        },
+      )
+      ..loadRequest(Uri.parse('https://flutter.dev'));
+
+    // #docregion platform_features
+    if (controller.platform is AndroidWebViewController) {
+      AndroidWebViewController.enableDebugging(true);
+      (controller.platform as AndroidWebViewController)
+          .setMediaPlaybackRequiresUserGesture(false);
+    }
+    // #enddocregion platform_features
+
+    _controller = controller;
   }
 
   @override
@@ -99,77 +167,25 @@
         title: const Text('Flutter WebView example'),
         // This drop down menu demonstrates that Flutter widgets can be shown over the web view.
         actions: <Widget>[
-          NavigationControls(_controller.future),
-          SampleMenu(_controller.future, widget.cookieManager),
+          NavigationControls(webViewController: _controller),
+          SampleMenu(webViewController: _controller),
         ],
       ),
-      body: WebView(
-        initialUrl: 'https://flutter.dev',
-        javascriptMode: JavascriptMode.unrestricted,
-        onWebViewCreated: (WebViewController webViewController) {
-          _controller.complete(webViewController);
-        },
-        onProgress: (int progress) {
-          print('WebView is loading (progress : $progress%)');
-        },
-        javascriptChannels: <JavascriptChannel>{
-          _toasterJavascriptChannel(context),
-        },
-        navigationDelegate: (NavigationRequest request) {
-          if (request.url.startsWith('https://www.youtube.com/')) {
-            print('blocking navigation to $request}');
-            return NavigationDecision.prevent;
-          }
-          print('allowing navigation to $request');
-          return NavigationDecision.navigate;
-        },
-        onPageStarted: (String url) {
-          print('Page started loading: $url');
-        },
-        onPageFinished: (String url) {
-          print('Page finished loading: $url');
-        },
-        gestureNavigationEnabled: true,
-        backgroundColor: const Color(0x00000000),
-      ),
+      body: WebViewWidget(controller: _controller),
       floatingActionButton: favoriteButton(),
     );
   }
 
-  JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
-    return JavascriptChannel(
-        name: 'Toaster',
-        onMessageReceived: (JavascriptMessage message) {
-          ScaffoldMessenger.of(context).showSnackBar(
-            SnackBar(content: Text(message.message)),
-          );
-        });
-  }
-
   Widget favoriteButton() {
-    return FutureBuilder<WebViewController>(
-        future: _controller.future,
-        builder: (BuildContext context,
-            AsyncSnapshot<WebViewController> controller) {
-          return FloatingActionButton(
-            onPressed: () async {
-              String? url;
-              if (controller.hasData) {
-                url = await controller.data!.currentUrl();
-              }
-              ScaffoldMessenger.of(context).showSnackBar(
-                SnackBar(
-                  content: Text(
-                    controller.hasData
-                        ? 'Favorited $url'
-                        : 'Unable to favorite',
-                  ),
-                ),
-              );
-            },
-            child: const Icon(Icons.favorite),
-          );
-        });
+    return FloatingActionButton(
+      onPressed: () async {
+        final String? url = await _controller.currentUrl();
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('Favorited $url')),
+        );
+      },
+      child: const Icon(Icons.favorite),
+    );
   }
 }
 
@@ -190,137 +206,130 @@
 }
 
 class SampleMenu extends StatelessWidget {
-  SampleMenu(this.controller, CookieManager? cookieManager, {Key? key})
-      : cookieManager = cookieManager ?? CookieManager(),
-        super(key: key);
+  SampleMenu({
+    super.key,
+    required this.webViewController,
+  });
 
-  final Future<WebViewController> controller;
-  late final CookieManager cookieManager;
+  final WebViewController webViewController;
+  late final WebViewCookieManager cookieManager = WebViewCookieManager();
 
   @override
   Widget build(BuildContext context) {
-    return FutureBuilder<WebViewController>(
-      future: controller,
-      builder:
-          (BuildContext context, AsyncSnapshot<WebViewController> controller) {
-        return PopupMenuButton<MenuOptions>(
-          key: const ValueKey<String>('ShowPopupMenu'),
-          onSelected: (MenuOptions value) {
-            switch (value) {
-              case MenuOptions.showUserAgent:
-                _onShowUserAgent(controller.data!, context);
-                break;
-              case MenuOptions.listCookies:
-                _onListCookies(controller.data!, context);
-                break;
-              case MenuOptions.clearCookies:
-                _onClearCookies(context);
-                break;
-              case MenuOptions.addToCache:
-                _onAddToCache(controller.data!, context);
-                break;
-              case MenuOptions.listCache:
-                _onListCache(controller.data!, context);
-                break;
-              case MenuOptions.clearCache:
-                _onClearCache(controller.data!, context);
-                break;
-              case MenuOptions.navigationDelegate:
-                _onNavigationDelegateExample(controller.data!, context);
-                break;
-              case MenuOptions.doPostRequest:
-                _onDoPostRequest(controller.data!, context);
-                break;
-              case MenuOptions.loadLocalFile:
-                _onLoadLocalFileExample(controller.data!, context);
-                break;
-              case MenuOptions.loadFlutterAsset:
-                _onLoadFlutterAssetExample(controller.data!, context);
-                break;
-              case MenuOptions.loadHtmlString:
-                _onLoadHtmlStringExample(controller.data!, context);
-                break;
-              case MenuOptions.transparentBackground:
-                _onTransparentBackground(controller.data!, context);
-                break;
-              case MenuOptions.setCookie:
-                _onSetCookie(controller.data!, context);
-                break;
-            }
-          },
-          itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
-            PopupMenuItem<MenuOptions>(
-              value: MenuOptions.showUserAgent,
-              enabled: controller.hasData,
-              child: const Text('Show user agent'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.listCookies,
-              child: Text('List cookies'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.clearCookies,
-              child: Text('Clear cookies'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.addToCache,
-              child: Text('Add to cache'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.listCache,
-              child: Text('List cache'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.clearCache,
-              child: Text('Clear cache'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.navigationDelegate,
-              child: Text('Navigation Delegate example'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.doPostRequest,
-              child: Text('Post Request'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.loadHtmlString,
-              child: Text('Load HTML string'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.loadLocalFile,
-              child: Text('Load local file'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.loadFlutterAsset,
-              child: Text('Load Flutter Asset'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              key: ValueKey<String>('ShowTransparentBackgroundExample'),
-              value: MenuOptions.transparentBackground,
-              child: Text('Transparent background example'),
-            ),
-            const PopupMenuItem<MenuOptions>(
-              value: MenuOptions.setCookie,
-              child: Text('Set cookie'),
-            ),
-          ],
-        );
+    return PopupMenuButton<MenuOptions>(
+      key: const ValueKey<String>('ShowPopupMenu'),
+      onSelected: (MenuOptions value) {
+        switch (value) {
+          case MenuOptions.showUserAgent:
+            _onShowUserAgent();
+            break;
+          case MenuOptions.listCookies:
+            _onListCookies(context);
+            break;
+          case MenuOptions.clearCookies:
+            _onClearCookies(context);
+            break;
+          case MenuOptions.addToCache:
+            _onAddToCache(context);
+            break;
+          case MenuOptions.listCache:
+            _onListCache();
+            break;
+          case MenuOptions.clearCache:
+            _onClearCache(context);
+            break;
+          case MenuOptions.navigationDelegate:
+            _onNavigationDelegateExample();
+            break;
+          case MenuOptions.doPostRequest:
+            _onDoPostRequest();
+            break;
+          case MenuOptions.loadLocalFile:
+            _onLoadLocalFileExample();
+            break;
+          case MenuOptions.loadFlutterAsset:
+            _onLoadFlutterAssetExample();
+            break;
+          case MenuOptions.loadHtmlString:
+            _onLoadHtmlStringExample();
+            break;
+          case MenuOptions.transparentBackground:
+            _onTransparentBackground();
+            break;
+          case MenuOptions.setCookie:
+            _onSetCookie();
+            break;
+        }
       },
+      itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.showUserAgent,
+          child: Text('Show user agent'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.listCookies,
+          child: Text('List cookies'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.clearCookies,
+          child: Text('Clear cookies'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.addToCache,
+          child: Text('Add to cache'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.listCache,
+          child: Text('List cache'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.clearCache,
+          child: Text('Clear cache'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.navigationDelegate,
+          child: Text('Navigation Delegate example'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.doPostRequest,
+          child: Text('Post Request'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.loadHtmlString,
+          child: Text('Load HTML string'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.loadLocalFile,
+          child: Text('Load local file'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.loadFlutterAsset,
+          child: Text('Load Flutter Asset'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          key: ValueKey<String>('ShowTransparentBackgroundExample'),
+          value: MenuOptions.transparentBackground,
+          child: Text('Transparent background example'),
+        ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.setCookie,
+          child: Text('Set cookie'),
+        ),
+      ],
     );
   }
 
-  Future<void> _onShowUserAgent(
-      WebViewController controller, BuildContext context) async {
+  Future<void> _onShowUserAgent() {
     // Send a message with the user agent string to the Toaster JavaScript channel we registered
     // with the WebView.
-    await controller.runJavascript(
-        'Toaster.postMessage("User Agent: " + navigator.userAgent);');
+    return webViewController.runJavaScript(
+      'Toaster.postMessage("User Agent: " + navigator.userAgent);',
+    );
   }
 
-  Future<void> _onListCookies(
-      WebViewController controller, BuildContext context) async {
-    final String cookies =
-        await controller.runJavascriptReturningResult('document.cookie');
+  Future<void> _onListCookies(BuildContext context) async {
+    final String cookies = await webViewController
+        .runJavaScriptReturningResult('document.cookie') as String;
     ScaffoldMessenger.of(context).showSnackBar(SnackBar(
       content: Column(
         mainAxisAlignment: MainAxisAlignment.end,
@@ -333,26 +342,25 @@
     ));
   }
 
-  Future<void> _onAddToCache(
-      WebViewController controller, BuildContext context) async {
-    await controller.runJavascript(
-        'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";');
+  Future<void> _onAddToCache(BuildContext context) async {
+    await webViewController.runJavaScript(
+      'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";',
+    );
     ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
       content: Text('Added a test entry to cache.'),
     ));
   }
 
-  Future<void> _onListCache(
-      WebViewController controller, BuildContext context) async {
-    await controller.runJavascript('caches.keys()'
+  Future<void> _onListCache() {
+    return webViewController.runJavaScript('caches.keys()'
         // ignore: missing_whitespace_between_adjacent_strings
         '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))'
         '.then((caches) => Toaster.postMessage(caches))');
   }
 
-  Future<void> _onClearCache(
-      WebViewController controller, BuildContext context) async {
-    await controller.clearCache();
+  Future<void> _onClearCache(BuildContext context) async {
+    await webViewController.clearCache();
+    await webViewController.clearLocalStorage();
     ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
       content: Text('Cache cleared.'),
     ));
@@ -369,53 +377,53 @@
     ));
   }
 
-  Future<void> _onNavigationDelegateExample(
-      WebViewController controller, BuildContext context) async {
-    final String contentBase64 =
-        base64Encode(const Utf8Encoder().convert(kNavigationExamplePage));
-    await controller.loadUrl('data:text/html;base64,$contentBase64');
+  Future<void> _onNavigationDelegateExample() {
+    final String contentBase64 = base64Encode(
+      const Utf8Encoder().convert(kNavigationExamplePage),
+    );
+    return webViewController.loadRequest(
+      Uri.parse('data:text/html;base64,$contentBase64'),
+    );
   }
 
-  Future<void> _onSetCookie(
-      WebViewController controller, BuildContext context) async {
+  Future<void> _onSetCookie() async {
     await cookieManager.setCookie(
       const WebViewCookie(
-          name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'),
+        name: 'foo',
+        value: 'bar',
+        domain: 'httpbin.org',
+        path: '/anything',
+      ),
     );
-    await controller.loadUrl('https://httpbin.org/anything');
+    await webViewController.loadRequest(Uri.parse(
+      'https://httpbin.org/anything',
+    ));
   }
 
-  Future<void> _onDoPostRequest(
-      WebViewController controller, BuildContext context) async {
-    final WebViewRequest request = WebViewRequest(
-      uri: Uri.parse('https://httpbin.org/post'),
-      method: WebViewRequestMethod.post,
+  Future<void> _onDoPostRequest() {
+    return webViewController.loadRequest(
+      Uri.parse('https://httpbin.org/post'),
+      method: LoadRequestMethod.post,
       headers: <String, String>{'foo': 'bar', 'Content-Type': 'text/plain'},
       body: Uint8List.fromList('Test Body'.codeUnits),
     );
-    await controller.loadRequest(request);
   }
 
-  Future<void> _onLoadLocalFileExample(
-      WebViewController controller, BuildContext context) async {
+  Future<void> _onLoadLocalFileExample() async {
     final String pathToIndex = await _prepareLocalFile();
-
-    await controller.loadFile(pathToIndex);
+    await webViewController.loadFile(pathToIndex);
   }
 
-  Future<void> _onLoadFlutterAssetExample(
-      WebViewController controller, BuildContext context) async {
-    await controller.loadFlutterAsset('assets/www/index.html');
+  Future<void> _onLoadFlutterAssetExample() {
+    return webViewController.loadFlutterAsset('assets/www/index.html');
   }
 
-  Future<void> _onLoadHtmlStringExample(
-      WebViewController controller, BuildContext context) async {
-    await controller.loadHtmlString(kLocalExamplePage);
+  Future<void> _onLoadHtmlStringExample() {
+    return webViewController.loadHtmlString(kLocalExamplePage);
   }
 
-  Future<void> _onTransparentBackground(
-      WebViewController controller, BuildContext context) async {
-    await controller.loadHtmlString(kTransparentBackgroundPage);
+  Future<void> _onTransparentBackground() {
+    return webViewController.loadHtmlString(kTransparentBackgroundPage);
   }
 
   Widget _getCookieList(String cookies) {
@@ -445,65 +453,45 @@
 }
 
 class NavigationControls extends StatelessWidget {
-  const NavigationControls(this._webViewControllerFuture, {Key? key})
-      : assert(_webViewControllerFuture != null),
-        super(key: key);
+  const NavigationControls({super.key, required this.webViewController});
 
-  final Future<WebViewController> _webViewControllerFuture;
+  final WebViewController webViewController;
 
   @override
   Widget build(BuildContext context) {
-    return FutureBuilder<WebViewController>(
-      future: _webViewControllerFuture,
-      builder:
-          (BuildContext context, AsyncSnapshot<WebViewController> snapshot) {
-        final bool webViewReady =
-            snapshot.connectionState == ConnectionState.done;
-        final WebViewController? controller = snapshot.data;
-        return Row(
-          children: <Widget>[
-            IconButton(
-              icon: const Icon(Icons.arrow_back_ios),
-              onPressed: !webViewReady
-                  ? null
-                  : () async {
-                      if (await controller!.canGoBack()) {
-                        await controller.goBack();
-                      } else {
-                        ScaffoldMessenger.of(context).showSnackBar(
-                          const SnackBar(content: Text('No back history item')),
-                        );
-                        return;
-                      }
-                    },
-            ),
-            IconButton(
-              icon: const Icon(Icons.arrow_forward_ios),
-              onPressed: !webViewReady
-                  ? null
-                  : () async {
-                      if (await controller!.canGoForward()) {
-                        await controller.goForward();
-                      } else {
-                        ScaffoldMessenger.of(context).showSnackBar(
-                          const SnackBar(
-                              content: Text('No forward history item')),
-                        );
-                        return;
-                      }
-                    },
-            ),
-            IconButton(
-              icon: const Icon(Icons.replay),
-              onPressed: !webViewReady
-                  ? null
-                  : () {
-                      controller!.reload();
-                    },
-            ),
-          ],
-        );
-      },
+    return Row(
+      children: <Widget>[
+        IconButton(
+          icon: const Icon(Icons.arrow_back_ios),
+          onPressed: () async {
+            if (await webViewController.canGoBack()) {
+              await webViewController.goBack();
+            } else {
+              ScaffoldMessenger.of(context).showSnackBar(
+                const SnackBar(content: Text('No back history item')),
+              );
+              return;
+            }
+          },
+        ),
+        IconButton(
+          icon: const Icon(Icons.arrow_forward_ios),
+          onPressed: () async {
+            if (await webViewController.canGoForward()) {
+              await webViewController.goForward();
+            } else {
+              ScaffoldMessenger.of(context).showSnackBar(
+                const SnackBar(content: Text('No forward history item')),
+              );
+              return;
+            }
+          },
+        ),
+        IconButton(
+          icon: const Icon(Icons.replay),
+          onPressed: () => webViewController.reload(),
+        ),
+      ],
     );
   }
 }
diff --git a/packages/webview_flutter/webview_flutter/example/lib/simple_example.dart b/packages/webview_flutter/webview_flutter/example/lib/simple_example.dart
new file mode 100644
index 0000000..dfee9e6
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/example/lib/simple_example.dart
@@ -0,0 +1,59 @@
+// 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.
+
+// ignore_for_file: public_member_api_docs
+
+import 'package:flutter/material.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+
+void main() => runApp(const MaterialApp(home: WebViewExample()));
+
+class WebViewExample extends StatefulWidget {
+  const WebViewExample({super.key});
+
+  @override
+  State<WebViewExample> createState() => _WebViewExampleState();
+}
+
+class _WebViewExampleState extends State<WebViewExample> {
+  late final WebViewController controller;
+
+  @override
+  void initState() {
+    super.initState();
+
+    // #docregion webview_controller
+    controller = WebViewController()
+      ..setJavaScriptMode(JavaScriptMode.unrestricted)
+      ..setBackgroundColor(const Color(0x00000000))
+      ..setNavigationDelegate(
+        NavigationDelegate(
+          onProgress: (int progress) {
+            // Update loading bar.
+          },
+          onPageStarted: (String url) {},
+          onPageFinished: (String url) {},
+          onWebResourceError: (WebResourceError error) {},
+          onNavigationRequest: (NavigationRequest request) {
+            if (request.url.startsWith('https://www.youtube.com/')) {
+              return NavigationDecision.prevent;
+            }
+            return NavigationDecision.navigate;
+          },
+        ),
+      )
+      ..loadRequest(Uri.parse('https://flutter.dev'));
+    // #enddocregion webview_controller
+  }
+
+  // #docregion webview_widget
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(title: const Text('Flutter Simple Example')),
+      body: WebViewWidget(controller: controller),
+    );
+  }
+  // #enddocregion webview_widget
+}
diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml
index 6b01b53..4d8d788 100644
--- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml
@@ -3,8 +3,8 @@
 publish_to: none
 
 environment:
-  sdk: ">=2.14.0 <3.0.0"
-  flutter: ">=2.10.0"
+  sdk: ">=2.17.0 <3.0.0"
+  flutter: ">=3.0.0"
 
 dependencies:
   flutter:
@@ -17,8 +17,11 @@
     # The example app is bundled with the plugin so we use a path dependency on
     # the parent directory to use the current plugin's version.
     path: ../
+  webview_flutter_android: ^3.0.0
+  webview_flutter_wkwebview: ^3.0.0
 
 dev_dependencies:
+  build_runner: ^2.1.5
   espresso: ^0.2.0
   flutter_driver:
     sdk: flutter
@@ -26,6 +29,7 @@
     sdk: flutter
   integration_test:
     sdk: flutter
+  webview_flutter_platform_interface: ^2.0.0
 
 flutter:
   uses-material-design: true
diff --git a/packages/webview_flutter/webview_flutter/example/test/main_test.dart b/packages/webview_flutter/webview_flutter/example/test/main_test.dart
index 8676333..7857022 100644
--- a/packages/webview_flutter/webview_flutter/example/test/main_test.dart
+++ b/packages/webview_flutter/webview_flutter/example/test/main_test.dart
@@ -4,17 +4,17 @@
 
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:webview_flutter/webview_flutter.dart';
 import 'package:webview_flutter_example/main.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
 
 void main() {
+  setUp(() {
+    WebViewPlatform.instance = FakeWebViewPlatform();
+  });
+
   testWidgets('Test snackbar from ScaffoldMessenger',
       (WidgetTester tester) async {
-    await tester.pumpWidget(
-      MaterialApp(
-        home: WebViewExample(cookieManager: FakeCookieManager()),
-      ),
-    );
+    await tester.pumpWidget(const MaterialApp(home: WebViewExample()));
     expect(find.byIcon(Icons.favorite), findsOneWidget);
     await tester.tap(find.byIcon(Icons.favorite));
     await tester.pump();
@@ -22,18 +22,95 @@
   });
 }
 
-class FakeCookieManager implements CookieManager {
-  factory FakeCookieManager() {
-    return _instance ??= FakeCookieManager._();
+class FakeWebViewPlatform extends WebViewPlatform {
+  @override
+  PlatformWebViewController createPlatformWebViewController(
+    PlatformWebViewControllerCreationParams params,
+  ) {
+    return FakeWebViewController(params);
   }
 
-  FakeCookieManager._();
-
-  static FakeCookieManager? _instance;
+  @override
+  PlatformWebViewWidget createPlatformWebViewWidget(
+    PlatformWebViewWidgetCreationParams params,
+  ) {
+    return FakeWebViewWidget(params);
+  }
 
   @override
-  Future<bool> clearCookies() => throw UnimplementedError();
+  PlatformWebViewCookieManager createPlatformCookieManager(
+    PlatformWebViewCookieManagerCreationParams params,
+  ) {
+    return FakeCookieManager(params);
+  }
 
   @override
-  Future<void> setCookie(WebViewCookie cookie) => throw UnimplementedError();
+  PlatformNavigationDelegate createPlatformNavigationDelegate(
+    PlatformNavigationDelegateCreationParams params,
+  ) {
+    return FakeNavigationDelegate(params);
+  }
+}
+
+class FakeWebViewController extends PlatformWebViewController {
+  FakeWebViewController(super.params) : super.implementation();
+
+  @override
+  Future<void> setJavaScriptMode(JavaScriptMode javaScriptMode) async {}
+
+  @override
+  Future<void> setBackgroundColor(Color color) async {}
+
+  @override
+  Future<void> setPlatformNavigationDelegate(
+    PlatformNavigationDelegate handler,
+  ) async {}
+
+  @override
+  Future<void> addJavaScriptChannel(
+      JavaScriptChannelParams javaScriptChannelParams) async {}
+
+  @override
+  Future<void> loadRequest(LoadRequestParams params) async {}
+
+  @override
+  Future<String?> currentUrl() async {
+    return 'https://www.google.com';
+  }
+}
+
+class FakeCookieManager extends PlatformWebViewCookieManager {
+  FakeCookieManager(super.params) : super.implementation();
+}
+
+class FakeWebViewWidget extends PlatformWebViewWidget {
+  FakeWebViewWidget(super.params) : super.implementation();
+
+  @override
+  Widget build(BuildContext context) {
+    return Container();
+  }
+}
+
+class FakeNavigationDelegate extends PlatformNavigationDelegate {
+  FakeNavigationDelegate(super.params) : super.implementation();
+
+  @override
+  Future<void> setOnNavigationRequest(
+    NavigationRequestCallback onNavigationRequest,
+  ) async {}
+
+  @override
+  Future<void> setOnPageFinished(PageEventCallback onPageFinished) async {}
+
+  @override
+  Future<void> setOnPageStarted(PageEventCallback onPageStarted) async {}
+
+  @override
+  Future<void> setOnProgress(ProgressCallback onProgress) async {}
+
+  @override
+  Future<void> setOnWebResourceError(
+    WebResourceErrorCallback onWebResourceError,
+  ) async {}
 }
diff --git a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/webview_flutter/lib/src/legacy/platform_interface.dart
similarity index 89%
rename from packages/webview_flutter/webview_flutter/lib/platform_interface.dart
rename to packages/webview_flutter/webview_flutter/lib/src/legacy/platform_interface.dart
index 48f7434..e036d2e 100644
--- a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart
+++ b/packages/webview_flutter/webview_flutter/lib/src/legacy/platform_interface.dart
@@ -5,7 +5,7 @@
 /// Re-export the classes from the webview_flutter_platform_interface through
 /// the `platform_interface.dart` file so we don't accidentally break any
 /// non-endorsed existing implementations of the interface.
-export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'
+export 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'
     show
         AutoMediaPlaybackPolicy,
         CreationParams,
diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart
similarity index 98%
rename from packages/webview_flutter/webview_flutter/lib/src/webview.dart
rename to packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart
index 7de8e28..8d7baa9 100644
--- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart
+++ b/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart
@@ -8,10 +8,12 @@
 import 'package:flutter/foundation.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/widgets.dart';
-import 'package:webview_flutter_android/webview_android_cookie_manager.dart';
-import 'package:webview_flutter_android/webview_surface_android.dart';
-import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
-import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
+// ignore: implementation_imports
+import 'package:webview_flutter_android/src/webview_flutter_android_legacy.dart';
+// ignore: implementation_imports
+import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart';
+// ignore: implementation_imports
+import 'package:webview_flutter_wkwebview/src/webview_flutter_wkwebview_legacy.dart';
 
 /// Optional callback invoked when a web view is first created. [controller] is
 /// the [WebViewController] for the created web view.
@@ -76,7 +78,7 @@
   ///
   /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null.
   const WebView({
-    Key? key,
+    super.key,
     this.onWebViewCreated,
     this.initialUrl,
     this.initialCookies = const <WebViewCookie>[],
@@ -98,8 +100,7 @@
     this.backgroundColor,
   })  : assert(javascriptMode != null),
         assert(initialMediaPlaybackPolicy != null),
-        assert(allowsInlineMediaPlayback != null),
-        super(key: key);
+        assert(allowsInlineMediaPlayback != null);
 
   static WebViewPlatform? _platform;
 
diff --git a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart
new file mode 100644
index 0000000..0651ad4
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart
@@ -0,0 +1,104 @@
+// 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:async';
+
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+
+import 'webview_controller.dart';
+
+/// Callbacks for accepting or rejecting navigation changes, and for tracking
+/// the progress of navigation requests.
+///
+/// See [WebViewController.setNavigationDelegate].
+class NavigationDelegate {
+  /// Constructs a [NavigationDelegate].
+  NavigationDelegate({
+    FutureOr<NavigationDecision> Function(NavigationRequest request)?
+        onNavigationRequest,
+    void Function(String url)? onPageStarted,
+    void Function(String url)? onPageFinished,
+    void Function(int progress)? onProgress,
+    void Function(WebResourceError error)? onWebResourceError,
+  }) : this.fromPlatformCreationParams(
+          const PlatformNavigationDelegateCreationParams(),
+          onNavigationRequest: onNavigationRequest,
+          onPageStarted: onPageStarted,
+          onPageFinished: onPageFinished,
+          onProgress: onProgress,
+          onWebResourceError: onWebResourceError,
+        );
+
+  /// Constructs a [NavigationDelegate] from creation params for a specific
+  /// platform.
+  NavigationDelegate.fromPlatformCreationParams(
+    PlatformNavigationDelegateCreationParams params, {
+    FutureOr<NavigationDecision> Function(NavigationRequest request)?
+        onNavigationRequest,
+    void Function(String url)? onPageStarted,
+    void Function(String url)? onPageFinished,
+    void Function(int progress)? onProgress,
+    void Function(WebResourceError error)? onWebResourceError,
+  }) : this.fromPlatform(
+          PlatformNavigationDelegate(params),
+          onNavigationRequest: onNavigationRequest,
+          onPageStarted: onPageStarted,
+          onPageFinished: onPageFinished,
+          onProgress: onProgress,
+          onWebResourceError: onWebResourceError,
+        );
+
+  /// Constructs a [NavigationDelegate] from a specific platform implementation.
+  NavigationDelegate.fromPlatform(
+    this.platform, {
+    this.onNavigationRequest,
+    this.onPageStarted,
+    this.onPageFinished,
+    this.onProgress,
+    this.onWebResourceError,
+  }) {
+    if (onNavigationRequest != null) {
+      platform.setOnNavigationRequest(onNavigationRequest!);
+    }
+    if (onPageStarted != null) {
+      platform.setOnPageStarted(onPageStarted!);
+    }
+    if (onPageFinished != null) {
+      platform.setOnPageFinished(onPageFinished!);
+    }
+    if (onProgress != null) {
+      platform.setOnProgress(onProgress!);
+    }
+    if (onWebResourceError != null) {
+      platform.setOnWebResourceError(onWebResourceError!);
+    }
+  }
+
+  /// Implementation of [PlatformNavigationDelegate] for the current platform.
+  final PlatformNavigationDelegate platform;
+
+  /// Invoked when a decision for a navigation request is pending.
+  ///
+  /// When a navigation is initiated by the WebView (e.g when a user clicks a
+  /// link) this delegate is called and has to decide how to proceed with the
+  /// navigation.
+  ///
+  /// *Important*: Some platforms may also trigger this callback from calls to
+  /// [WebViewController.loadRequest].
+  ///
+  /// See [NavigationDecision].
+  final NavigationRequestCallback? onNavigationRequest;
+
+  /// Invoked when a page has started loading.
+  final PageEventCallback? onPageStarted;
+
+  /// Invoked when a page has finished loading.
+  final PageEventCallback? onPageFinished;
+
+  /// Invoked when a page is loading to report the progress.
+  final ProgressCallback? onProgress;
+
+  /// Invoked when a resource loading error occurred.
+  final WebResourceErrorCallback? onWebResourceError;
+}
diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/src/v4/webview_flutter.dart
deleted file mode 100644
index f4a0b20..0000000
--- a/packages/webview_flutter/webview_flutter/lib/src/v4/webview_flutter.dart
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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;
-
-export 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'
-    show JavaScriptMessage, LoadRequestMethod, WebViewCookie;
-
-export 'src/webview_controller.dart';
-export 'src/webview_cookie_manager.dart';
-export 'src/webview_widget.dart';
diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_controller.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart
similarity index 94%
rename from packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_controller.dart
rename to packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart
index bd03b24..d632d1e 100644
--- a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_controller.dart
+++ b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart
@@ -2,14 +2,14 @@
 // 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:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+
+import 'navigation_delegate.dart';
 
 /// Controls a WebView provided by the host platform.
 ///
@@ -125,6 +125,12 @@
     return platform.reload();
   }
 
+  /// Sets the [NavigationDelegate] containing the callback methods that are
+  /// called during navigation events.
+  Future<void> setNavigationDelegate(NavigationDelegate delegate) {
+    return platform.setPlatformNavigationDelegate(delegate.platform);
+  }
+
   /// Clears all caches used by the WebView.
   ///
   /// The following caches are cleared:
@@ -233,9 +239,8 @@
   /// Returns the current scroll position of this view.
   ///
   /// Scroll position is measured from the top left.
-  Future<Offset> getScrollPosition() async {
-    final Point<int> position = await platform.getScrollPosition();
-    return Offset(position.x.toDouble(), position.y.toDouble());
+  Future<Offset> getScrollPosition() {
+    return platform.getScrollPosition();
   }
 
   /// Whether to support zooming using the on-screen zoom controls and gestures.
diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart
similarity index 93%
rename from packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_cookie_manager.dart
rename to packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart
index a1091fa..bffa1b5 100644
--- a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_cookie_manager.dart
+++ b/packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart
@@ -2,7 +2,7 @@
 // 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 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
 
 /// Manages cookies pertaining to all WebViews.
 class WebViewCookieManager {
diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_flutter_legacy.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_flutter_legacy.dart
new file mode 100644
index 0000000..d040fc2
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/lib/src/webview_flutter_legacy.dart
@@ -0,0 +1,9 @@
+// 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.
+
+export 'package:webview_flutter_android/src/webview_flutter_android_legacy.dart';
+export 'package:webview_flutter_wkwebview/src/webview_flutter_wkwebview_legacy.dart';
+
+export 'legacy/platform_interface.dart';
+export 'legacy/webview.dart';
diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_widget.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart
similarity index 92%
rename from packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_widget.dart
rename to packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart
index 06e4f78..b318011 100644
--- a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_widget.dart
+++ b/packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart
@@ -5,7 +5,7 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/gestures.dart';
-import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
 
 import 'webview_controller.dart';
 
@@ -35,8 +35,7 @@
   }) : this.fromPlatform(key: key, platform: PlatformWebViewWidget(params));
 
   /// Constructs a [WebViewWidget] from a specific platform implementation.
-  WebViewWidget.fromPlatform({Key? key, required this.platform})
-      : super(key: key);
+  WebViewWidget.fromPlatform({super.key, required this.platform});
 
   /// Implementation of [PlatformWebViewWidget] for the current platform.
   final PlatformWebViewWidget platform;
diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart
index ba38771..7b8301d 100644
--- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart
+++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart
@@ -2,9 +2,28 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-export 'package:webview_flutter_android/webview_android.dart';
-export 'package:webview_flutter_android/webview_surface_android.dart';
-export 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
+library webview_flutter;
 
-export 'platform_interface.dart';
-export 'src/webview.dart';
+export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'
+    show
+        JavaScriptMessage,
+        JavaScriptMode,
+        LoadRequestMethod,
+        NavigationDecision,
+        NavigationRequest,
+        NavigationRequestCallback,
+        PageEventCallback,
+        PlatformNavigationDelegateCreationParams,
+        PlatformWebViewControllerCreationParams,
+        PlatformWebViewCookieManagerCreationParams,
+        PlatformWebViewWidgetCreationParams,
+        ProgressCallback,
+        WebResourceError,
+        WebResourceErrorCallback,
+        WebViewCookie,
+        WebViewPlatform;
+
+export 'src/navigation_delegate.dart';
+export 'src/webview_controller.dart';
+export 'src/webview_cookie_manager.dart';
+export 'src/webview_widget.dart';
diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml
index a02b032..58c0411 100644
--- a/packages/webview_flutter/webview_flutter/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter/pubspec.yaml
@@ -2,11 +2,11 @@
 description: A Flutter plugin that provides a WebView widget on Android and iOS.
 repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
-version: 3.0.4
+version: 4.0.0
 
 environment:
-  sdk: ">=2.14.0 <3.0.0"
-  flutter: ">=2.10.0"
+  sdk: ">=2.17.0 <3.0.0"
+  flutter: ">=3.0.0"
 
 flutter:
   plugin:
@@ -19,9 +19,9 @@
 dependencies:
   flutter:
     sdk: flutter
-  webview_flutter_android: ^2.8.0
-  webview_flutter_platform_interface: ^1.9.3
-  webview_flutter_wkwebview: ^2.7.0
+  webview_flutter_android: ^3.0.0
+  webview_flutter_platform_interface: ^2.0.0
+  webview_flutter_wkwebview: ^3.0.0
 
 dev_dependencies:
   build_runner: ^2.1.5
@@ -29,4 +29,5 @@
     sdk: flutter
   flutter_test:
     sdk: flutter
-  mockito: ^5.0.16
+  mockito: ^5.3.2
+  plugin_platform_interface: ^2.1.3
diff --git a/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.dart
new file mode 100644
index 0000000..4db7011
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.dart
@@ -0,0 +1,1367 @@
+// 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:typed_data';
+
+import 'package:flutter/src/foundation/basic_types.dart';
+import 'package:flutter/src/gestures/recognizer.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:webview_flutter/src/webview_flutter_legacy.dart';
+import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart';
+
+import 'webview_flutter_test.mocks.dart';
+
+typedef VoidCallback = void Function();
+
+@GenerateMocks(<Type>[WebViewPlatform, WebViewPlatformController])
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  late MockWebViewPlatform mockWebViewPlatform;
+  late MockWebViewPlatformController mockWebViewPlatformController;
+  late MockWebViewCookieManagerPlatform mockWebViewCookieManagerPlatform;
+
+  setUp(() {
+    mockWebViewPlatformController = MockWebViewPlatformController();
+    mockWebViewPlatform = MockWebViewPlatform();
+    mockWebViewCookieManagerPlatform = MockWebViewCookieManagerPlatform();
+    when(mockWebViewPlatform.build(
+      context: anyNamed('context'),
+      creationParams: anyNamed('creationParams'),
+      webViewPlatformCallbacksHandler:
+          anyNamed('webViewPlatformCallbacksHandler'),
+      javascriptChannelRegistry: anyNamed('javascriptChannelRegistry'),
+      onWebViewPlatformCreated: anyNamed('onWebViewPlatformCreated'),
+      gestureRecognizers: anyNamed('gestureRecognizers'),
+    )).thenAnswer((Invocation invocation) {
+      final WebViewPlatformCreatedCallback onWebViewPlatformCreated =
+          invocation.namedArguments[const Symbol('onWebViewPlatformCreated')]
+              as WebViewPlatformCreatedCallback;
+      return TestPlatformWebView(
+        mockWebViewPlatformController: mockWebViewPlatformController,
+        onWebViewPlatformCreated: onWebViewPlatformCreated,
+      );
+    });
+
+    WebView.platform = mockWebViewPlatform;
+    WebViewCookieManagerPlatform.instance = mockWebViewCookieManagerPlatform;
+  });
+
+  tearDown(() {
+    mockWebViewCookieManagerPlatform.reset();
+  });
+
+  testWidgets('Create WebView', (WidgetTester tester) async {
+    await tester.pumpWidget(const WebView());
+  });
+
+  testWidgets('Initial url', (WidgetTester tester) async {
+    await tester.pumpWidget(const WebView(initialUrl: 'https://youtube.com'));
+
+    final CreationParams params = captureBuildArgs(
+      mockWebViewPlatform,
+      creationParams: true,
+    ).single as CreationParams;
+
+    expect(params.initialUrl, 'https://youtube.com');
+  });
+
+  testWidgets('Javascript mode', (WidgetTester tester) async {
+    await tester.pumpWidget(const WebView(
+      javascriptMode: JavascriptMode.unrestricted,
+    ));
+
+    final CreationParams unrestrictedparams = captureBuildArgs(
+      mockWebViewPlatform,
+      creationParams: true,
+    ).single as CreationParams;
+
+    expect(
+      unrestrictedparams.webSettings!.javascriptMode,
+      JavascriptMode.unrestricted,
+    );
+
+    await tester.pumpWidget(const WebView());
+
+    final CreationParams disabledparams = captureBuildArgs(
+      mockWebViewPlatform,
+      creationParams: true,
+    ).single as CreationParams;
+
+    expect(disabledparams.webSettings!.javascriptMode, JavascriptMode.disabled);
+  });
+
+  testWidgets('Load file', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    await controller!.loadFile('/test/path/index.html');
+
+    verify(mockWebViewPlatformController.loadFile(
+      '/test/path/index.html',
+    ));
+  });
+
+  testWidgets('Load file with empty path', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    expect(() => controller!.loadFile(''), throwsAssertionError);
+  });
+
+  testWidgets('Load Flutter asset', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    await controller!.loadFlutterAsset('assets/index.html');
+
+    verify(mockWebViewPlatformController.loadFlutterAsset(
+      'assets/index.html',
+    ));
+  });
+
+  testWidgets('Load Flutter asset with empty key', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    expect(() => controller!.loadFlutterAsset(''), throwsAssertionError);
+  });
+
+  testWidgets('Load HTML string without base URL', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    await controller!.loadHtmlString('<p>This is a test paragraph.</p>');
+
+    verify(mockWebViewPlatformController.loadHtmlString(
+      '<p>This is a test paragraph.</p>',
+    ));
+  });
+
+  testWidgets('Load HTML string with base URL', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    await controller!.loadHtmlString(
+      '<p>This is a test paragraph.</p>',
+      baseUrl: 'https://flutter.dev',
+    );
+
+    verify(mockWebViewPlatformController.loadHtmlString(
+      '<p>This is a test paragraph.</p>',
+      baseUrl: 'https://flutter.dev',
+    ));
+  });
+
+  testWidgets('Load HTML string with empty string',
+      (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    expect(() => controller!.loadHtmlString(''), throwsAssertionError);
+  });
+
+  testWidgets('Load url', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    await controller!.loadUrl('https://flutter.io');
+
+    verify(mockWebViewPlatformController.loadUrl(
+      'https://flutter.io',
+      argThat(isNull),
+    ));
+  });
+
+  testWidgets('Invalid urls', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    final CreationParams params = captureBuildArgs(
+      mockWebViewPlatform,
+      creationParams: true,
+    ).single as CreationParams;
+
+    expect(params.initialUrl, isNull);
+
+    expect(() => controller!.loadUrl(''), throwsA(anything));
+    expect(() => controller!.loadUrl('flutter.io'), throwsA(anything));
+  });
+
+  testWidgets('Headers in loadUrl', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    final Map<String, String> headers = <String, String>{
+      'CACHE-CONTROL': 'ABC'
+    };
+    await controller!.loadUrl('https://flutter.io', headers: headers);
+
+    verify(mockWebViewPlatformController.loadUrl(
+      'https://flutter.io',
+      <String, String>{'CACHE-CONTROL': 'ABC'},
+    ));
+  });
+
+  testWidgets('loadRequest', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+    expect(controller, isNotNull);
+
+    final WebViewRequest req = WebViewRequest(
+      uri: Uri.parse('https://flutter.dev'),
+      method: WebViewRequestMethod.post,
+      headers: <String, String>{'foo': 'bar'},
+      body: Uint8List.fromList('Test Body'.codeUnits),
+    );
+
+    await controller!.loadRequest(req);
+
+    verify(mockWebViewPlatformController.loadRequest(req));
+  });
+
+  testWidgets('Clear Cache', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+
+    await controller!.clearCache();
+
+    verify(mockWebViewPlatformController.clearCache());
+  });
+
+  testWidgets('Can go back', (WidgetTester tester) async {
+    when(mockWebViewPlatformController.canGoBack())
+        .thenAnswer((_) => Future<bool>.value(true));
+
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+    expect(controller!.canGoBack(), completion(true));
+  });
+
+  testWidgets("Can't go forward", (WidgetTester tester) async {
+    when(mockWebViewPlatformController.canGoForward())
+        .thenAnswer((_) => Future<bool>.value(false));
+
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+    expect(controller!.canGoForward(), completion(false));
+  });
+
+  testWidgets('Go back', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://youtube.com',
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    await controller!.goBack();
+    verify(mockWebViewPlatformController.goBack());
+  });
+
+  testWidgets('Go forward', (WidgetTester tester) async {
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://youtube.com',
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+    await controller!.goForward();
+    verify(mockWebViewPlatformController.goForward());
+  });
+
+  testWidgets('Current URL', (WidgetTester tester) async {
+    when(mockWebViewPlatformController.currentUrl())
+        .thenAnswer((_) => Future<String>.value('https://youtube.com'));
+
+    WebViewController? controller;
+    await tester.pumpWidget(
+      WebView(
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(controller, isNotNull);
+    expect(await controller!.currentUrl(), 'https://youtube.com');
+  });
+
+  testWidgets('Reload url', (WidgetTester tester) async {
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    await controller.reload();
+    verify(mockWebViewPlatformController.reload());
+  });
+
+  testWidgets('evaluate Javascript', (WidgetTester tester) async {
+    when(mockWebViewPlatformController.evaluateJavascript('fake js string'))
+        .thenAnswer((_) => Future<String>.value('fake js string'));
+
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        javascriptMode: JavascriptMode.unrestricted,
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+
+    expect(
+        // ignore: deprecated_member_use_from_same_package
+        await controller.evaluateJavascript('fake js string'),
+        'fake js string',
+        reason: 'should get the argument');
+  });
+
+  testWidgets('evaluate Javascript with JavascriptMode disabled',
+      (WidgetTester tester) async {
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+    expect(
+      // ignore: deprecated_member_use_from_same_package
+      () => controller.evaluateJavascript('fake js string'),
+      throwsA(anything),
+    );
+  });
+
+  testWidgets('runJavaScript', (WidgetTester tester) async {
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        javascriptMode: JavascriptMode.unrestricted,
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+    await controller.runJavascript('fake js string');
+    verify(mockWebViewPlatformController.runJavascript('fake js string'));
+  });
+
+  testWidgets('runJavaScript with JavascriptMode disabled',
+      (WidgetTester tester) async {
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+    expect(
+      () => controller.runJavascript('fake js string'),
+      throwsA(anything),
+    );
+  });
+
+  testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async {
+    when(mockWebViewPlatformController
+            .runJavascriptReturningResult('fake js string'))
+        .thenAnswer((_) => Future<String>.value('fake js string'));
+
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        javascriptMode: JavascriptMode.unrestricted,
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+    expect(await controller.runJavascriptReturningResult('fake js string'),
+        'fake js string',
+        reason: 'should get the argument');
+  });
+
+  testWidgets('runJavaScriptReturningResult with JavascriptMode disabled',
+      (WidgetTester tester) async {
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+    expect(
+      () => controller.runJavascriptReturningResult('fake js string'),
+      throwsA(anything),
+    );
+  });
+
+  testWidgets('Cookies can be cleared once', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const WebView(
+        initialUrl: 'https://flutter.io',
+      ),
+    );
+    final CookieManager cookieManager = CookieManager();
+    final bool hasCookies = await cookieManager.clearCookies();
+    expect(hasCookies, true);
+  });
+
+  testWidgets('Cookies can be set', (WidgetTester tester) async {
+    const WebViewCookie cookie =
+        WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev');
+
+    await tester.pumpWidget(
+      const WebView(
+        initialUrl: 'https://flutter.io',
+      ),
+    );
+    final CookieManager cookieManager = CookieManager();
+    await cookieManager.setCookie(cookie);
+    expect(mockWebViewCookieManagerPlatform.setCookieCalls,
+        <WebViewCookie>[cookie]);
+  });
+
+  testWidgets('Initial JavaScript channels', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://youtube.com',
+        javascriptChannels: <JavascriptChannel>{
+          JavascriptChannel(
+              name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
+          JavascriptChannel(
+              name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
+        },
+      ),
+    );
+
+    final CreationParams params = captureBuildArgs(
+      mockWebViewPlatform,
+      creationParams: true,
+    ).single as CreationParams;
+
+    expect(params.javascriptChannelNames,
+        unorderedEquals(<String>['Tts', 'Alarm']));
+  });
+
+  test('Only valid JavaScript channel names are allowed', () {
+    void noOp(JavascriptMessage msg) {}
+    JavascriptChannel(name: 'Tts1', onMessageReceived: noOp);
+    JavascriptChannel(name: '_Alarm', onMessageReceived: noOp);
+    JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp);
+
+    VoidCallback createChannel(String name) {
+      return () {
+        JavascriptChannel(name: name, onMessageReceived: noOp);
+      };
+    }
+
+    expect(createChannel('1Alarm'), throwsAssertionError);
+    expect(createChannel('foo.bar'), throwsAssertionError);
+    expect(createChannel(''), throwsAssertionError);
+  });
+
+  testWidgets('Unique JavaScript channel names are required',
+      (WidgetTester tester) async {
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://youtube.com',
+        javascriptChannels: <JavascriptChannel>{
+          JavascriptChannel(
+              name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
+          JavascriptChannel(
+              name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
+        },
+      ),
+    );
+    expect(tester.takeException(), isNot(null));
+  });
+
+  testWidgets('JavaScript channels update', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://youtube.com',
+        javascriptChannels: <JavascriptChannel>{
+          JavascriptChannel(
+              name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
+          JavascriptChannel(
+              name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
+        },
+      ),
+    );
+
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://youtube.com',
+        javascriptChannels: <JavascriptChannel>{
+          JavascriptChannel(
+              name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
+          JavascriptChannel(
+              name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}),
+          JavascriptChannel(
+              name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}),
+        },
+      ),
+    );
+
+    final JavascriptChannelRegistry channelRegistry = captureBuildArgs(
+      mockWebViewPlatform,
+      javascriptChannelRegistry: true,
+    ).first as JavascriptChannelRegistry;
+
+    expect(
+      channelRegistry.channels.keys,
+      unorderedEquals(<String>['Tts', 'Alarm2', 'Alarm3']),
+    );
+  });
+
+  testWidgets('Remove all JavaScript channels and then add',
+      (WidgetTester tester) async {
+    // This covers a specific bug we had where after updating javascriptChannels to null,
+    // updating it again with a subset of the previously registered channels fails as the
+    // widget's cache of current channel wasn't properly updated when updating javascriptChannels to
+    // null.
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://youtube.com',
+        javascriptChannels: <JavascriptChannel>{
+          JavascriptChannel(
+              name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
+        },
+      ),
+    );
+
+    await tester.pumpWidget(
+      const WebView(
+        initialUrl: 'https://youtube.com',
+      ),
+    );
+
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://youtube.com',
+        javascriptChannels: <JavascriptChannel>{
+          JavascriptChannel(
+              name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
+        },
+      ),
+    );
+
+    final JavascriptChannelRegistry channelRegistry = captureBuildArgs(
+      mockWebViewPlatform,
+      javascriptChannelRegistry: true,
+    ).last as JavascriptChannelRegistry;
+
+    expect(channelRegistry.channels.keys, unorderedEquals(<String>['Tts']));
+  });
+
+  testWidgets('JavaScript channel messages', (WidgetTester tester) async {
+    final List<String> ttsMessagesReceived = <String>[];
+    final List<String> alarmMessagesReceived = <String>[];
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://youtube.com',
+        javascriptChannels: <JavascriptChannel>{
+          JavascriptChannel(
+              name: 'Tts',
+              onMessageReceived: (JavascriptMessage msg) {
+                ttsMessagesReceived.add(msg.message);
+              }),
+          JavascriptChannel(
+              name: 'Alarm',
+              onMessageReceived: (JavascriptMessage msg) {
+                alarmMessagesReceived.add(msg.message);
+              }),
+        },
+      ),
+    );
+
+    final JavascriptChannelRegistry channelRegistry = captureBuildArgs(
+      mockWebViewPlatform,
+      javascriptChannelRegistry: true,
+    ).single as JavascriptChannelRegistry;
+
+    expect(ttsMessagesReceived, isEmpty);
+    expect(alarmMessagesReceived, isEmpty);
+
+    channelRegistry.onJavascriptChannelMessage('Tts', 'Hello');
+    channelRegistry.onJavascriptChannelMessage('Tts', 'World');
+
+    expect(ttsMessagesReceived, <String>['Hello', 'World']);
+  });
+
+  group('$PageStartedCallback', () {
+    testWidgets('onPageStarted is not null', (WidgetTester tester) async {
+      String? returnedUrl;
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        onPageStarted: (String url) {
+          returnedUrl = url;
+        },
+      ));
+
+      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
+        mockWebViewPlatform,
+        webViewPlatformCallbacksHandler: true,
+      ).single as WebViewPlatformCallbacksHandler;
+
+      handler.onPageStarted('https://youtube.com');
+
+      expect(returnedUrl, 'https://youtube.com');
+    });
+
+    testWidgets('onPageStarted is null', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView(
+        initialUrl: 'https://youtube.com',
+      ));
+
+      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
+        mockWebViewPlatform,
+        webViewPlatformCallbacksHandler: true,
+      ).single as WebViewPlatformCallbacksHandler;
+
+      // The platform side will always invoke a call for onPageStarted. This is
+      // to test that it does not crash on a null callback.
+      handler.onPageStarted('https://youtube.com');
+    });
+
+    testWidgets('onPageStarted changed', (WidgetTester tester) async {
+      String? returnedUrl;
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        onPageStarted: (String url) {},
+      ));
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        onPageStarted: (String url) {
+          returnedUrl = url;
+        },
+      ));
+
+      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
+        mockWebViewPlatform,
+        webViewPlatformCallbacksHandler: true,
+      ).last as WebViewPlatformCallbacksHandler;
+      handler.onPageStarted('https://youtube.com');
+
+      expect(returnedUrl, 'https://youtube.com');
+    });
+  });
+
+  group('$PageFinishedCallback', () {
+    testWidgets('onPageFinished is not null', (WidgetTester tester) async {
+      String? returnedUrl;
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        onPageFinished: (String url) {
+          returnedUrl = url;
+        },
+      ));
+
+      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
+        mockWebViewPlatform,
+        webViewPlatformCallbacksHandler: true,
+      ).single as WebViewPlatformCallbacksHandler;
+      handler.onPageFinished('https://youtube.com');
+
+      expect(returnedUrl, 'https://youtube.com');
+    });
+
+    testWidgets('onPageFinished is null', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView(
+        initialUrl: 'https://youtube.com',
+      ));
+
+      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
+        mockWebViewPlatform,
+        webViewPlatformCallbacksHandler: true,
+      ).single as WebViewPlatformCallbacksHandler;
+      // The platform side will always invoke a call for onPageFinished. This is
+      // to test that it does not crash on a null callback.
+      handler.onPageFinished('https://youtube.com');
+    });
+
+    testWidgets('onPageFinished changed', (WidgetTester tester) async {
+      String? returnedUrl;
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        onPageFinished: (String url) {},
+      ));
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        onPageFinished: (String url) {
+          returnedUrl = url;
+        },
+      ));
+
+      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
+        mockWebViewPlatform,
+        webViewPlatformCallbacksHandler: true,
+      ).last as WebViewPlatformCallbacksHandler;
+      handler.onPageFinished('https://youtube.com');
+
+      expect(returnedUrl, 'https://youtube.com');
+    });
+  });
+
+  group('$PageLoadingCallback', () {
+    testWidgets('onLoadingProgress is not null', (WidgetTester tester) async {
+      int? loadingProgress;
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        onProgress: (int progress) {
+          loadingProgress = progress;
+        },
+      ));
+
+      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
+        mockWebViewPlatform,
+        webViewPlatformCallbacksHandler: true,
+      ).single as WebViewPlatformCallbacksHandler;
+      handler.onProgress(50);
+
+      expect(loadingProgress, 50);
+    });
+
+    testWidgets('onLoadingProgress is null', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView(
+        initialUrl: 'https://youtube.com',
+      ));
+
+      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
+        mockWebViewPlatform,
+        webViewPlatformCallbacksHandler: true,
+      ).single as WebViewPlatformCallbacksHandler;
+
+      // This is to test that it does not crash on a null callback.
+      handler.onProgress(50);
+    });
+
+    testWidgets('onLoadingProgress changed', (WidgetTester tester) async {
+      int? loadingProgress;
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        onProgress: (int progress) {},
+      ));
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        onProgress: (int progress) {
+          loadingProgress = progress;
+        },
+      ));
+
+      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
+        mockWebViewPlatform,
+        webViewPlatformCallbacksHandler: true,
+      ).last as WebViewPlatformCallbacksHandler;
+      handler.onProgress(50);
+
+      expect(loadingProgress, 50);
+    });
+  });
+
+  group('navigationDelegate', () {
+    testWidgets('hasNavigationDelegate', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView(
+        initialUrl: 'https://youtube.com',
+      ));
+
+      final CreationParams params = captureBuildArgs(
+        mockWebViewPlatform,
+        creationParams: true,
+      ).single as CreationParams;
+
+      expect(params.webSettings!.hasNavigationDelegate, false);
+
+      await tester.pumpWidget(WebView(
+        initialUrl: 'https://youtube.com',
+        navigationDelegate: (NavigationRequest r) =>
+            NavigationDecision.navigate,
+      ));
+
+      final WebSettings updateSettings =
+          verify(mockWebViewPlatformController.updateSettings(captureAny))
+              .captured
+              .single as WebSettings;
+
+      expect(updateSettings.hasNavigationDelegate, true);
+    });
+
+    testWidgets('Block navigation', (WidgetTester tester) async {
+      final List<NavigationRequest> navigationRequests = <NavigationRequest>[];
+
+      await tester.pumpWidget(WebView(
+          initialUrl: 'https://youtube.com',
+          navigationDelegate: (NavigationRequest request) {
+            navigationRequests.add(request);
+            // Only allow navigating to https://flutter.dev
+            return request.url == 'https://flutter.dev'
+                ? NavigationDecision.navigate
+                : NavigationDecision.prevent;
+          }));
+
+      final List<dynamic> args = captureBuildArgs(
+        mockWebViewPlatform,
+        creationParams: true,
+        webViewPlatformCallbacksHandler: true,
+      );
+
+      final CreationParams params = args[0] as CreationParams;
+      expect(params.webSettings!.hasNavigationDelegate, true);
+
+      final WebViewPlatformCallbacksHandler handler =
+          args[1] as WebViewPlatformCallbacksHandler;
+
+      // The navigation delegate only allows navigation to https://flutter.dev
+      // so we should still be in https://youtube.com.
+      expect(
+        handler.onNavigationRequest(
+          url: 'https://www.google.com',
+          isForMainFrame: true,
+        ),
+        completion(false),
+      );
+
+      expect(navigationRequests.length, 1);
+      expect(navigationRequests[0].url, 'https://www.google.com');
+      expect(navigationRequests[0].isForMainFrame, true);
+
+      expect(
+        handler.onNavigationRequest(
+          url: 'https://flutter.dev',
+          isForMainFrame: true,
+        ),
+        completion(true),
+      );
+    });
+  });
+
+  group('debuggingEnabled', () {
+    testWidgets('enable debugging', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView(
+        debuggingEnabled: true,
+      ));
+
+      final CreationParams params = captureBuildArgs(
+        mockWebViewPlatform,
+        creationParams: true,
+      ).single as CreationParams;
+
+      expect(params.webSettings!.debuggingEnabled, true);
+    });
+
+    testWidgets('defaults to false', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView());
+
+      final CreationParams params = captureBuildArgs(
+        mockWebViewPlatform,
+        creationParams: true,
+      ).single as CreationParams;
+
+      expect(params.webSettings!.debuggingEnabled, false);
+    });
+
+    testWidgets('can be changed', (WidgetTester tester) async {
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(WebView(key: key));
+
+      await tester.pumpWidget(WebView(
+        key: key,
+        debuggingEnabled: true,
+      ));
+
+      final WebSettings enabledSettings =
+          verify(mockWebViewPlatformController.updateSettings(captureAny))
+              .captured
+              .last as WebSettings;
+      expect(enabledSettings.debuggingEnabled, true);
+
+      await tester.pumpWidget(WebView(
+        key: key,
+      ));
+
+      final WebSettings disabledSettings =
+          verify(mockWebViewPlatformController.updateSettings(captureAny))
+              .captured
+              .last as WebSettings;
+      expect(disabledSettings.debuggingEnabled, false);
+    });
+  });
+
+  group('zoomEnabled', () {
+    testWidgets('Enable zoom', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView());
+
+      final CreationParams params = captureBuildArgs(
+        mockWebViewPlatform,
+        creationParams: true,
+      ).single as CreationParams;
+
+      expect(params.webSettings!.zoomEnabled, isTrue);
+    });
+
+    testWidgets('defaults to true', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView());
+
+      final CreationParams params = captureBuildArgs(
+        mockWebViewPlatform,
+        creationParams: true,
+      ).single as CreationParams;
+
+      expect(params.webSettings!.zoomEnabled, isTrue);
+    });
+
+    testWidgets('can be changed', (WidgetTester tester) async {
+      final GlobalKey key = GlobalKey();
+      await tester.pumpWidget(WebView(key: key));
+
+      await tester.pumpWidget(WebView(
+        key: key,
+      ));
+
+      final WebSettings enabledSettings =
+          verify(mockWebViewPlatformController.updateSettings(captureAny))
+              .captured
+              .last as WebSettings;
+      // Zoom defaults to true, so no changes are made to settings.
+      expect(enabledSettings.zoomEnabled, isNull);
+
+      await tester.pumpWidget(WebView(
+        key: key,
+        zoomEnabled: false,
+      ));
+
+      final WebSettings disabledSettings =
+          verify(mockWebViewPlatformController.updateSettings(captureAny))
+              .captured
+              .last as WebSettings;
+      expect(disabledSettings.zoomEnabled, isFalse);
+    });
+  });
+
+  group('Background color', () {
+    testWidgets('Defaults to null', (WidgetTester tester) async {
+      await tester.pumpWidget(const WebView());
+
+      final CreationParams params = captureBuildArgs(
+        mockWebViewPlatform,
+        creationParams: true,
+      ).single as CreationParams;
+
+      expect(params.backgroundColor, null);
+    });
+
+    testWidgets('Can be transparent', (WidgetTester tester) async {
+      const Color transparentColor = Color(0x00000000);
+
+      await tester.pumpWidget(const WebView(
+        backgroundColor: transparentColor,
+      ));
+
+      final CreationParams params = captureBuildArgs(
+        mockWebViewPlatform,
+        creationParams: true,
+      ).single as CreationParams;
+
+      expect(params.backgroundColor, transparentColor);
+    });
+  });
+
+  group('Custom platform implementation', () {
+    setUp(() {
+      WebView.platform = MyWebViewPlatform();
+    });
+    tearDownAll(() {
+      WebView.platform = null;
+    });
+
+    testWidgets('creation', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        const WebView(
+          initialUrl: 'https://youtube.com',
+          gestureNavigationEnabled: true,
+        ),
+      );
+
+      final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform;
+      final MyWebViewPlatformController platform = builder.lastPlatformBuilt!;
+
+      expect(
+          platform.creationParams,
+          MatchesCreationParams(CreationParams(
+            initialUrl: 'https://youtube.com',
+            webSettings: WebSettings(
+              javascriptMode: JavascriptMode.disabled,
+              hasNavigationDelegate: false,
+              debuggingEnabled: false,
+              userAgent: const WebSetting<String?>.of(null),
+              gestureNavigationEnabled: true,
+              zoomEnabled: true,
+            ),
+          )));
+    });
+
+    testWidgets('loadUrl', (WidgetTester tester) async {
+      late WebViewController controller;
+      await tester.pumpWidget(
+        WebView(
+          initialUrl: 'https://youtube.com',
+          onWebViewCreated: (WebViewController webViewController) {
+            controller = webViewController;
+          },
+        ),
+      );
+
+      final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform;
+      final MyWebViewPlatformController platform = builder.lastPlatformBuilt!;
+
+      final Map<String, String> headers = <String, String>{
+        'header': 'value',
+      };
+
+      await controller.loadUrl('https://google.com', headers: headers);
+
+      expect(platform.lastUrlLoaded, 'https://google.com');
+      expect(platform.lastRequestHeaders, headers);
+    });
+  });
+
+  testWidgets('Set UserAgent', (WidgetTester tester) async {
+    await tester.pumpWidget(const WebView(
+      initialUrl: 'https://youtube.com',
+      javascriptMode: JavascriptMode.unrestricted,
+    ));
+
+    final CreationParams params = captureBuildArgs(
+      mockWebViewPlatform,
+      creationParams: true,
+    ).single as CreationParams;
+
+    expect(params.webSettings!.userAgent.value, isNull);
+
+    await tester.pumpWidget(const WebView(
+      initialUrl: 'https://youtube.com',
+      javascriptMode: JavascriptMode.unrestricted,
+      userAgent: 'UA',
+    ));
+
+    final WebSettings settings =
+        verify(mockWebViewPlatformController.updateSettings(captureAny))
+            .captured
+            .last as WebSettings;
+    expect(settings.userAgent.value, 'UA');
+  });
+}
+
+List<dynamic> captureBuildArgs(
+  MockWebViewPlatform mockWebViewPlatform, {
+  bool context = false,
+  bool creationParams = false,
+  bool webViewPlatformCallbacksHandler = false,
+  bool javascriptChannelRegistry = false,
+  bool onWebViewPlatformCreated = false,
+  bool gestureRecognizers = false,
+}) {
+  return verify(mockWebViewPlatform.build(
+    context: context ? captureAnyNamed('context') : anyNamed('context'),
+    creationParams: creationParams
+        ? captureAnyNamed('creationParams')
+        : anyNamed('creationParams'),
+    webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler
+        ? captureAnyNamed('webViewPlatformCallbacksHandler')
+        : anyNamed('webViewPlatformCallbacksHandler'),
+    javascriptChannelRegistry: javascriptChannelRegistry
+        ? captureAnyNamed('javascriptChannelRegistry')
+        : anyNamed('javascriptChannelRegistry'),
+    onWebViewPlatformCreated: onWebViewPlatformCreated
+        ? captureAnyNamed('onWebViewPlatformCreated')
+        : anyNamed('onWebViewPlatformCreated'),
+    gestureRecognizers: gestureRecognizers
+        ? captureAnyNamed('gestureRecognizers')
+        : anyNamed('gestureRecognizers'),
+  )).captured;
+}
+
+// This Widget ensures that onWebViewPlatformCreated is only called once when
+// making multiple calls to `WidgetTester.pumpWidget` with different parameters
+// for the WebView.
+class TestPlatformWebView extends StatefulWidget {
+  const TestPlatformWebView({
+    super.key,
+    required this.mockWebViewPlatformController,
+    this.onWebViewPlatformCreated,
+  });
+
+  final MockWebViewPlatformController mockWebViewPlatformController;
+  final WebViewPlatformCreatedCallback? onWebViewPlatformCreated;
+
+  @override
+  State<StatefulWidget> createState() => TestPlatformWebViewState();
+}
+
+class TestPlatformWebViewState extends State<TestPlatformWebView> {
+  @override
+  void initState() {
+    super.initState();
+    final WebViewPlatformCreatedCallback? onWebViewPlatformCreated =
+        widget.onWebViewPlatformCreated;
+    if (onWebViewPlatformCreated != null) {
+      onWebViewPlatformCreated(widget.mockWebViewPlatformController);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container();
+  }
+}
+
+class MyWebViewPlatform implements WebViewPlatform {
+  MyWebViewPlatformController? lastPlatformBuilt;
+
+  @override
+  Widget build({
+    BuildContext? context,
+    CreationParams? creationParams,
+    required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
+    required JavascriptChannelRegistry javascriptChannelRegistry,
+    WebViewPlatformCreatedCallback? onWebViewPlatformCreated,
+    Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
+  }) {
+    assert(onWebViewPlatformCreated != null);
+    lastPlatformBuilt = MyWebViewPlatformController(
+        creationParams, gestureRecognizers, webViewPlatformCallbacksHandler);
+    onWebViewPlatformCreated!(lastPlatformBuilt);
+    return Container();
+  }
+
+  @override
+  Future<bool> clearCookies() {
+    return Future<bool>.sync(() => true);
+  }
+}
+
+class MyWebViewPlatformController extends WebViewPlatformController {
+  MyWebViewPlatformController(this.creationParams, this.gestureRecognizers,
+      WebViewPlatformCallbacksHandler platformHandler)
+      : super(platformHandler);
+
+  CreationParams? creationParams;
+  Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers;
+
+  String? lastUrlLoaded;
+  Map<String, String>? lastRequestHeaders;
+
+  @override
+  Future<void> loadUrl(String url, Map<String, String>? headers) async {
+    equals(1, 1);
+    lastUrlLoaded = url;
+    lastRequestHeaders = headers;
+  }
+}
+
+class MatchesWebSettings extends Matcher {
+  MatchesWebSettings(this._webSettings);
+
+  final WebSettings? _webSettings;
+
+  @override
+  Description describe(Description description) =>
+      description.add('$_webSettings');
+
+  @override
+  bool matches(
+      covariant WebSettings webSettings, Map<dynamic, dynamic> matchState) {
+    return _webSettings!.javascriptMode == webSettings.javascriptMode &&
+        _webSettings!.hasNavigationDelegate ==
+            webSettings.hasNavigationDelegate &&
+        _webSettings!.debuggingEnabled == webSettings.debuggingEnabled &&
+        _webSettings!.gestureNavigationEnabled ==
+            webSettings.gestureNavigationEnabled &&
+        _webSettings!.userAgent == webSettings.userAgent &&
+        _webSettings!.zoomEnabled == webSettings.zoomEnabled;
+  }
+}
+
+class MatchesCreationParams extends Matcher {
+  MatchesCreationParams(this._creationParams);
+
+  final CreationParams _creationParams;
+
+  @override
+  Description describe(Description description) =>
+      description.add('$_creationParams');
+
+  @override
+  bool matches(covariant CreationParams creationParams,
+      Map<dynamic, dynamic> matchState) {
+    return _creationParams.initialUrl == creationParams.initialUrl &&
+        MatchesWebSettings(_creationParams.webSettings)
+            .matches(creationParams.webSettings!, matchState) &&
+        orderedEquals(_creationParams.javascriptChannelNames)
+            .matches(creationParams.javascriptChannelNames, matchState);
+  }
+}
+
+class MockWebViewCookieManagerPlatform extends WebViewCookieManagerPlatform {
+  List<WebViewCookie> setCookieCalls = <WebViewCookie>[];
+
+  @override
+  Future<bool> clearCookies() async => true;
+
+  @override
+  Future<void> setCookie(WebViewCookie cookie) async {
+    setCookieCalls.add(cookie);
+  }
+
+  void reset() {
+    setCookieCalls = <WebViewCookie>[];
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart
new file mode 100644
index 0000000..a40cf34
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart
@@ -0,0 +1,346 @@
+// Mocks generated by Mockito 5.3.2 from annotations
+// in webview_flutter/test/legacy/webview_flutter_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i9;
+
+import 'package:flutter/foundation.dart' as _i3;
+import 'package:flutter/gestures.dart' as _i8;
+import 'package:flutter/widgets.dart' as _i2;
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:webview_flutter_platform_interface/src/legacy/platform_interface/javascript_channel_registry.dart'
+    as _i7;
+import 'package:webview_flutter_platform_interface/src/legacy/platform_interface/webview_platform.dart'
+    as _i4;
+import 'package:webview_flutter_platform_interface/src/legacy/platform_interface/webview_platform_callbacks_handler.dart'
+    as _i6;
+import 'package:webview_flutter_platform_interface/src/legacy/platform_interface/webview_platform_controller.dart'
+    as _i10;
+import 'package:webview_flutter_platform_interface/src/legacy/types/types.dart'
+    as _i5;
+
+// 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
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakeWidget_0 extends _i1.SmartFake implements _i2.Widget {
+  _FakeWidget_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+
+  @override
+  String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) =>
+      super.toString();
+}
+
+/// A class which mocks [WebViewPlatform].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockWebViewPlatform extends _i1.Mock implements _i4.WebViewPlatform {
+  MockWebViewPlatform() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i2.Widget build({
+    required _i2.BuildContext? context,
+    required _i5.CreationParams? creationParams,
+    required _i6.WebViewPlatformCallbacksHandler?
+        webViewPlatformCallbacksHandler,
+    required _i7.JavascriptChannelRegistry? javascriptChannelRegistry,
+    _i4.WebViewPlatformCreatedCallback? onWebViewPlatformCreated,
+    Set<_i3.Factory<_i8.OneSequenceGestureRecognizer>>? gestureRecognizers,
+  }) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #build,
+          [],
+          {
+            #context: context,
+            #creationParams: creationParams,
+            #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler,
+            #javascriptChannelRegistry: javascriptChannelRegistry,
+            #onWebViewPlatformCreated: onWebViewPlatformCreated,
+            #gestureRecognizers: gestureRecognizers,
+          },
+        ),
+        returnValue: _FakeWidget_0(
+          this,
+          Invocation.method(
+            #build,
+            [],
+            {
+              #context: context,
+              #creationParams: creationParams,
+              #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler,
+              #javascriptChannelRegistry: javascriptChannelRegistry,
+              #onWebViewPlatformCreated: onWebViewPlatformCreated,
+              #gestureRecognizers: gestureRecognizers,
+            },
+          ),
+        ),
+      ) as _i2.Widget);
+  @override
+  _i9.Future<bool> clearCookies() => (super.noSuchMethod(
+        Invocation.method(
+          #clearCookies,
+          [],
+        ),
+        returnValue: _i9.Future<bool>.value(false),
+      ) as _i9.Future<bool>);
+}
+
+/// A class which mocks [WebViewPlatformController].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockWebViewPlatformController extends _i1.Mock
+    implements _i10.WebViewPlatformController {
+  MockWebViewPlatformController() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i9.Future<void> loadFile(String? absoluteFilePath) => (super.noSuchMethod(
+        Invocation.method(
+          #loadFile,
+          [absoluteFilePath],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> loadFlutterAsset(String? key) => (super.noSuchMethod(
+        Invocation.method(
+          #loadFlutterAsset,
+          [key],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> loadHtmlString(
+    String? html, {
+    String? baseUrl,
+  }) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #loadHtmlString,
+          [html],
+          {#baseUrl: baseUrl},
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> loadUrl(
+    String? url,
+    Map<String, String>? headers,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #loadUrl,
+          [
+            url,
+            headers,
+          ],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> loadRequest(_i5.WebViewRequest? request) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #loadRequest,
+          [request],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> updateSettings(_i5.WebSettings? setting) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #updateSettings,
+          [setting],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<String?> currentUrl() => (super.noSuchMethod(
+        Invocation.method(
+          #currentUrl,
+          [],
+        ),
+        returnValue: _i9.Future<String?>.value(),
+      ) as _i9.Future<String?>);
+  @override
+  _i9.Future<bool> canGoBack() => (super.noSuchMethod(
+        Invocation.method(
+          #canGoBack,
+          [],
+        ),
+        returnValue: _i9.Future<bool>.value(false),
+      ) as _i9.Future<bool>);
+  @override
+  _i9.Future<bool> canGoForward() => (super.noSuchMethod(
+        Invocation.method(
+          #canGoForward,
+          [],
+        ),
+        returnValue: _i9.Future<bool>.value(false),
+      ) as _i9.Future<bool>);
+  @override
+  _i9.Future<void> goBack() => (super.noSuchMethod(
+        Invocation.method(
+          #goBack,
+          [],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> goForward() => (super.noSuchMethod(
+        Invocation.method(
+          #goForward,
+          [],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> reload() => (super.noSuchMethod(
+        Invocation.method(
+          #reload,
+          [],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> clearCache() => (super.noSuchMethod(
+        Invocation.method(
+          #clearCache,
+          [],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<String> evaluateJavascript(String? javascript) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #evaluateJavascript,
+          [javascript],
+        ),
+        returnValue: _i9.Future<String>.value(''),
+      ) as _i9.Future<String>);
+  @override
+  _i9.Future<void> runJavascript(String? javascript) => (super.noSuchMethod(
+        Invocation.method(
+          #runJavascript,
+          [javascript],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<String> runJavascriptReturningResult(String? javascript) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #runJavascriptReturningResult,
+          [javascript],
+        ),
+        returnValue: _i9.Future<String>.value(''),
+      ) as _i9.Future<String>);
+  @override
+  _i9.Future<void> addJavascriptChannels(Set<String>? javascriptChannelNames) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #addJavascriptChannels,
+          [javascriptChannelNames],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> removeJavascriptChannels(
+          Set<String>? javascriptChannelNames) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #removeJavascriptChannels,
+          [javascriptChannelNames],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<String?> getTitle() => (super.noSuchMethod(
+        Invocation.method(
+          #getTitle,
+          [],
+        ),
+        returnValue: _i9.Future<String?>.value(),
+      ) as _i9.Future<String?>);
+  @override
+  _i9.Future<void> scrollTo(
+    int? x,
+    int? y,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #scrollTo,
+          [
+            x,
+            y,
+          ],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<void> scrollBy(
+    int? x,
+    int? y,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #scrollBy,
+          [
+            x,
+            y,
+          ],
+        ),
+        returnValue: _i9.Future<void>.value(),
+        returnValueForMissingStub: _i9.Future<void>.value(),
+      ) as _i9.Future<void>);
+  @override
+  _i9.Future<int> getScrollX() => (super.noSuchMethod(
+        Invocation.method(
+          #getScrollX,
+          [],
+        ),
+        returnValue: _i9.Future<int>.value(0),
+      ) as _i9.Future<int>);
+  @override
+  _i9.Future<int> getScrollY() => (super.noSuchMethod(
+        Invocation.method(
+          #getScrollY,
+          [],
+        ),
+        returnValue: _i9.Future<int>.value(0),
+      ) as _i9.Future<int>);
+}
diff --git a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart
new file mode 100644
index 0000000..839454e
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart
@@ -0,0 +1,91 @@
+// 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:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+
+import 'navigation_delegate_test.mocks.dart';
+
+@GenerateMocks(<Type>[WebViewPlatform, PlatformNavigationDelegate])
+void main() {
+  group('NavigationDelegate', () {
+    test('onNavigationRequest', () async {
+      WebViewPlatform.instance = TestWebViewPlatform();
+
+      NavigationDecision onNavigationRequest(NavigationRequest request) {
+        return NavigationDecision.navigate;
+      }
+
+      final NavigationDelegate delegate = NavigationDelegate(
+        onNavigationRequest: onNavigationRequest,
+      );
+
+      verify(delegate.platform.setOnNavigationRequest(onNavigationRequest));
+    });
+
+    test('onPageStarted', () async {
+      WebViewPlatform.instance = TestWebViewPlatform();
+
+      void onPageStarted(String url) {}
+
+      final NavigationDelegate delegate = NavigationDelegate(
+        onPageStarted: onPageStarted,
+      );
+
+      verify(delegate.platform.setOnPageStarted(onPageStarted));
+    });
+
+    test('onPageFinished', () async {
+      WebViewPlatform.instance = TestWebViewPlatform();
+
+      void onPageFinished(String url) {}
+
+      final NavigationDelegate delegate = NavigationDelegate(
+        onPageFinished: onPageFinished,
+      );
+
+      verify(delegate.platform.setOnPageFinished(onPageFinished));
+    });
+
+    test('onProgress', () async {
+      WebViewPlatform.instance = TestWebViewPlatform();
+
+      void onProgress(int progress) {}
+
+      final NavigationDelegate delegate = NavigationDelegate(
+        onProgress: onProgress,
+      );
+
+      verify(delegate.platform.setOnProgress(onProgress));
+    });
+
+    test('onWebResourceError', () async {
+      WebViewPlatform.instance = TestWebViewPlatform();
+
+      void onWebResourceError(WebResourceError error) {}
+
+      final NavigationDelegate delegate = NavigationDelegate(
+        onWebResourceError: onWebResourceError,
+      );
+
+      verify(delegate.platform.setOnWebResourceError(onWebResourceError));
+    });
+  });
+}
+
+class TestWebViewPlatform extends WebViewPlatform {
+  @override
+  PlatformNavigationDelegate createPlatformNavigationDelegate(
+    PlatformNavigationDelegateCreationParams params,
+  ) {
+    return TestMockPlatformNavigationDelegate();
+  }
+}
+
+class TestMockPlatformNavigationDelegate extends MockPlatformNavigationDelegate
+    with MockPlatformInterfaceMixin {}
diff --git a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart
new file mode 100644
index 0000000..a7ac41e
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart
@@ -0,0 +1,231 @@
+// Mocks generated by Mockito 5.3.2 from annotations
+// in webview_flutter/test/navigation_delegate_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i8;
+
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart'
+    as _i3;
+import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart'
+    as _i4;
+import 'package:webview_flutter_platform_interface/src/platform_webview_cookie_manager.dart'
+    as _i2;
+import 'package:webview_flutter_platform_interface/src/platform_webview_widget.dart'
+    as _i5;
+import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i6;
+import 'package:webview_flutter_platform_interface/src/webview_platform.dart'
+    as _i7;
+
+// 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
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakePlatformWebViewCookieManager_0 extends _i1.SmartFake
+    implements _i2.PlatformWebViewCookieManager {
+  _FakePlatformWebViewCookieManager_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakePlatformNavigationDelegate_1 extends _i1.SmartFake
+    implements _i3.PlatformNavigationDelegate {
+  _FakePlatformNavigationDelegate_1(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakePlatformWebViewController_2 extends _i1.SmartFake
+    implements _i4.PlatformWebViewController {
+  _FakePlatformWebViewController_2(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakePlatformWebViewWidget_3 extends _i1.SmartFake
+    implements _i5.PlatformWebViewWidget {
+  _FakePlatformWebViewWidget_3(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakePlatformNavigationDelegateCreationParams_4 extends _i1.SmartFake
+    implements _i6.PlatformNavigationDelegateCreationParams {
+  _FakePlatformNavigationDelegateCreationParams_4(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+/// A class which mocks [WebViewPlatform].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockWebViewPlatform extends _i1.Mock implements _i7.WebViewPlatform {
+  MockWebViewPlatform() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i2.PlatformWebViewCookieManager createPlatformCookieManager(
+          _i6.PlatformWebViewCookieManagerCreationParams? params) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #createPlatformCookieManager,
+          [params],
+        ),
+        returnValue: _FakePlatformWebViewCookieManager_0(
+          this,
+          Invocation.method(
+            #createPlatformCookieManager,
+            [params],
+          ),
+        ),
+      ) as _i2.PlatformWebViewCookieManager);
+  @override
+  _i3.PlatformNavigationDelegate createPlatformNavigationDelegate(
+          _i6.PlatformNavigationDelegateCreationParams? params) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #createPlatformNavigationDelegate,
+          [params],
+        ),
+        returnValue: _FakePlatformNavigationDelegate_1(
+          this,
+          Invocation.method(
+            #createPlatformNavigationDelegate,
+            [params],
+          ),
+        ),
+      ) as _i3.PlatformNavigationDelegate);
+  @override
+  _i4.PlatformWebViewController createPlatformWebViewController(
+          _i6.PlatformWebViewControllerCreationParams? params) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #createPlatformWebViewController,
+          [params],
+        ),
+        returnValue: _FakePlatformWebViewController_2(
+          this,
+          Invocation.method(
+            #createPlatformWebViewController,
+            [params],
+          ),
+        ),
+      ) as _i4.PlatformWebViewController);
+  @override
+  _i5.PlatformWebViewWidget createPlatformWebViewWidget(
+          _i6.PlatformWebViewWidgetCreationParams? params) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #createPlatformWebViewWidget,
+          [params],
+        ),
+        returnValue: _FakePlatformWebViewWidget_3(
+          this,
+          Invocation.method(
+            #createPlatformWebViewWidget,
+            [params],
+          ),
+        ),
+      ) as _i5.PlatformWebViewWidget);
+}
+
+/// A class which mocks [PlatformNavigationDelegate].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockPlatformNavigationDelegate extends _i1.Mock
+    implements _i3.PlatformNavigationDelegate {
+  MockPlatformNavigationDelegate() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i6.PlatformNavigationDelegateCreationParams get params =>
+      (super.noSuchMethod(
+        Invocation.getter(#params),
+        returnValue: _FakePlatformNavigationDelegateCreationParams_4(
+          this,
+          Invocation.getter(#params),
+        ),
+      ) as _i6.PlatformNavigationDelegateCreationParams);
+  @override
+  _i8.Future<void> setOnNavigationRequest(
+          _i3.NavigationRequestCallback? onNavigationRequest) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnNavigationRequest,
+          [onNavigationRequest],
+        ),
+        returnValue: _i8.Future<void>.value(),
+        returnValueForMissingStub: _i8.Future<void>.value(),
+      ) as _i8.Future<void>);
+  @override
+  _i8.Future<void> setOnPageStarted(_i3.PageEventCallback? onPageStarted) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnPageStarted,
+          [onPageStarted],
+        ),
+        returnValue: _i8.Future<void>.value(),
+        returnValueForMissingStub: _i8.Future<void>.value(),
+      ) as _i8.Future<void>);
+  @override
+  _i8.Future<void> setOnPageFinished(_i3.PageEventCallback? onPageFinished) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnPageFinished,
+          [onPageFinished],
+        ),
+        returnValue: _i8.Future<void>.value(),
+        returnValueForMissingStub: _i8.Future<void>.value(),
+      ) as _i8.Future<void>);
+  @override
+  _i8.Future<void> setOnProgress(_i3.ProgressCallback? onProgress) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnProgress,
+          [onProgress],
+        ),
+        returnValue: _i8.Future<void>.value(),
+        returnValueForMissingStub: _i8.Future<void>.value(),
+      ) as _i8.Future<void>);
+  @override
+  _i8.Future<void> setOnWebResourceError(
+          _i3.WebResourceErrorCallback? onWebResourceError) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnWebResourceError,
+          [onWebResourceError],
+        ),
+        returnValue: _i8.Future<void>.value(),
+        returnValueForMissingStub: _i8.Future<void>.value(),
+      ) as _i8.Future<void>);
+}
diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.mocks.dart
deleted file mode 100644
index f0fb4b4..0000000
--- a/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.mocks.dart
+++ /dev/null
@@ -1,203 +0,0 @@
-// Mocks generated by Mockito 5.3.0 from annotations
-// in webview_flutter/test/v4/webview_controller_test.dart.
-// Do not manually edit this file.
-
-// ignore_for_file: no_leading_underscores_for_library_prefixes
-import 'dart:async' as _i5;
-import 'dart:math' as _i3;
-import 'dart:ui' as _i7;
-
-import 'package:mockito/mockito.dart' as _i1;
-import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart'
-    as _i6;
-import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart'
-    as _i4;
-import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'
-    as _i2;
-
-// 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
-// ignore_for_file: subtype_of_sealed_class
-
-class _FakePlatformWebViewControllerCreationParams_0 extends _i1.SmartFake
-    implements _i2.PlatformWebViewControllerCreationParams {
-  _FakePlatformWebViewControllerCreationParams_0(
-      Object parent, Invocation parentInvocation)
-      : super(parent, parentInvocation);
-}
-
-class _FakePoint_1<T extends num> extends _i1.SmartFake
-    implements _i3.Point<T> {
-  _FakePoint_1(Object parent, Invocation parentInvocation)
-      : super(parent, parentInvocation);
-}
-
-/// A class which mocks [PlatformWebViewController].
-///
-/// See the documentation for Mockito's code generation for more information.
-class MockPlatformWebViewController extends _i1.Mock
-    implements _i4.PlatformWebViewController {
-  MockPlatformWebViewController() {
-    _i1.throwOnMissingStub(this);
-  }
-
-  @override
-  _i2.PlatformWebViewControllerCreationParams get params =>
-      (super.noSuchMethod(Invocation.getter(#params),
-              returnValue: _FakePlatformWebViewControllerCreationParams_0(
-                  this, Invocation.getter(#params)))
-          as _i2.PlatformWebViewControllerCreationParams);
-  @override
-  _i5.Future<void> loadFile(String? absoluteFilePath) => (super.noSuchMethod(
-      Invocation.method(#loadFile, [absoluteFilePath]),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> loadFlutterAsset(String? key) => (super.noSuchMethod(
-      Invocation.method(#loadFlutterAsset, [key]),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> loadHtmlString(String? html, {String? baseUrl}) =>
-      (super.noSuchMethod(
-              Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}),
-              returnValue: _i5.Future<void>.value(),
-              returnValueForMissingStub: _i5.Future<void>.value())
-          as _i5.Future<void>);
-  @override
-  _i5.Future<void> loadRequest(_i2.LoadRequestParams? params) =>
-      (super.noSuchMethod(Invocation.method(#loadRequest, [params]),
-              returnValue: _i5.Future<void>.value(),
-              returnValueForMissingStub: _i5.Future<void>.value())
-          as _i5.Future<void>);
-  @override
-  _i5.Future<String?> currentUrl() =>
-      (super.noSuchMethod(Invocation.method(#currentUrl, []),
-          returnValue: _i5.Future<String?>.value()) as _i5.Future<String?>);
-  @override
-  _i5.Future<bool> canGoBack() =>
-      (super.noSuchMethod(Invocation.method(#canGoBack, []),
-          returnValue: _i5.Future<bool>.value(false)) as _i5.Future<bool>);
-  @override
-  _i5.Future<bool> canGoForward() =>
-      (super.noSuchMethod(Invocation.method(#canGoForward, []),
-          returnValue: _i5.Future<bool>.value(false)) as _i5.Future<bool>);
-  @override
-  _i5.Future<void> goBack() => (super.noSuchMethod(
-      Invocation.method(#goBack, []),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> goForward() => (super.noSuchMethod(
-      Invocation.method(#goForward, []),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> reload() => (super.noSuchMethod(
-      Invocation.method(#reload, []),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> clearCache() => (super.noSuchMethod(
-      Invocation.method(#clearCache, []),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> clearLocalStorage() => (super.noSuchMethod(
-      Invocation.method(#clearLocalStorage, []),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> setPlatformNavigationDelegate(
-          _i6.PlatformNavigationDelegate? handler) =>
-      (super.noSuchMethod(
-              Invocation.method(#setPlatformNavigationDelegate, [handler]),
-              returnValue: _i5.Future<void>.value(),
-              returnValueForMissingStub: _i5.Future<void>.value())
-          as _i5.Future<void>);
-  @override
-  _i5.Future<void> runJavaScript(String? javaScript) => (super.noSuchMethod(
-      Invocation.method(#runJavaScript, [javaScript]),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<String> runJavaScriptReturningResult(String? javaScript) =>
-      (super.noSuchMethod(
-          Invocation.method(#runJavaScriptReturningResult, [javaScript]),
-          returnValue: _i5.Future<String>.value('')) as _i5.Future<String>);
-  @override
-  _i5.Future<void> addJavaScriptChannel(
-          _i4.JavaScriptChannelParams? javaScriptChannelParams) =>
-      (super.noSuchMethod(
-          Invocation.method(#addJavaScriptChannel, [javaScriptChannelParams]),
-          returnValue: _i5.Future<void>.value(),
-          returnValueForMissingStub:
-              _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> removeJavaScriptChannel(String? javaScriptChannelName) =>
-      (super.noSuchMethod(
-          Invocation.method(#removeJavaScriptChannel, [javaScriptChannelName]),
-          returnValue: _i5.Future<void>.value(),
-          returnValueForMissingStub:
-              _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<String?> getTitle() =>
-      (super.noSuchMethod(Invocation.method(#getTitle, []),
-          returnValue: _i5.Future<String?>.value()) as _i5.Future<String?>);
-  @override
-  _i5.Future<void> scrollTo(int? x, int? y) => (super.noSuchMethod(
-      Invocation.method(#scrollTo, [x, y]),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> scrollBy(int? x, int? y) => (super.noSuchMethod(
-      Invocation.method(#scrollBy, [x, y]),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<_i3.Point<int>> getScrollPosition() =>
-      (super.noSuchMethod(Invocation.method(#getScrollPosition, []),
-              returnValue: _i5.Future<_i3.Point<int>>.value(_FakePoint_1<int>(
-                  this, Invocation.method(#getScrollPosition, []))))
-          as _i5.Future<_i3.Point<int>>);
-  @override
-  _i5.Future<void> enableDebugging(bool? enabled) => (super.noSuchMethod(
-      Invocation.method(#enableDebugging, [enabled]),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> enableGestureNavigation(bool? enabled) => (super
-          .noSuchMethod(Invocation.method(#enableGestureNavigation, [enabled]),
-              returnValue: _i5.Future<void>.value(),
-              returnValueForMissingStub: _i5.Future<void>.value())
-      as _i5.Future<void>);
-  @override
-  _i5.Future<void> enableZoom(bool? enabled) => (super.noSuchMethod(
-      Invocation.method(#enableZoom, [enabled]),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> setBackgroundColor(_i7.Color? color) => (super.noSuchMethod(
-      Invocation.method(#setBackgroundColor, [color]),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-  @override
-  _i5.Future<void> setJavaScriptMode(_i2.JavaScriptMode? javaScriptMode) =>
-      (super.noSuchMethod(
-              Invocation.method(#setJavaScriptMode, [javaScriptMode]),
-              returnValue: _i5.Future<void>.value(),
-              returnValueForMissingStub: _i5.Future<void>.value())
-          as _i5.Future<void>);
-  @override
-  _i5.Future<void> setUserAgent(String? userAgent) => (super.noSuchMethod(
-      Invocation.method(#setUserAgent, [userAgent]),
-      returnValue: _i5.Future<void>.value(),
-      returnValueForMissingStub: _i5.Future<void>.value()) as _i5.Future<void>);
-}
diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.mocks.dart
deleted file mode 100644
index 4bca8b6..0000000
--- a/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.mocks.dart
+++ /dev/null
@@ -1,56 +0,0 @@
-// Mocks generated by Mockito 5.3.0 from annotations
-// in webview_flutter/test/v4/webview_cookie_manager_test.dart.
-// Do not manually edit this file.
-
-// ignore_for_file: no_leading_underscores_for_library_prefixes
-import 'dart:async' as _i4;
-
-import 'package:mockito/mockito.dart' as _i1;
-import 'package:webview_flutter_platform_interface/v4/src/platform_webview_cookie_manager.dart'
-    as _i3;
-import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'
-    as _i2;
-
-// 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
-// ignore_for_file: subtype_of_sealed_class
-
-class _FakePlatformWebViewCookieManagerCreationParams_0 extends _i1.SmartFake
-    implements _i2.PlatformWebViewCookieManagerCreationParams {
-  _FakePlatformWebViewCookieManagerCreationParams_0(
-      Object parent, Invocation parentInvocation)
-      : super(parent, parentInvocation);
-}
-
-/// A class which mocks [PlatformWebViewCookieManager].
-///
-/// See the documentation for Mockito's code generation for more information.
-class MockPlatformWebViewCookieManager extends _i1.Mock
-    implements _i3.PlatformWebViewCookieManager {
-  MockPlatformWebViewCookieManager() {
-    _i1.throwOnMissingStub(this);
-  }
-
-  @override
-  _i2.PlatformWebViewCookieManagerCreationParams get params =>
-      (super.noSuchMethod(Invocation.getter(#params),
-              returnValue: _FakePlatformWebViewCookieManagerCreationParams_0(
-                  this, Invocation.getter(#params)))
-          as _i2.PlatformWebViewCookieManagerCreationParams);
-  @override
-  _i4.Future<bool> clearCookies() =>
-      (super.noSuchMethod(Invocation.method(#clearCookies, []),
-          returnValue: _i4.Future<bool>.value(false)) as _i4.Future<bool>);
-  @override
-  _i4.Future<void> setCookie(_i2.WebViewCookie? cookie) => (super.noSuchMethod(
-      Invocation.method(#setCookie, [cookie]),
-      returnValue: _i4.Future<void>.value(),
-      returnValueForMissingStub: _i4.Future<void>.value()) as _i4.Future<void>);
-}
diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.mocks.dart
deleted file mode 100644
index e481d75..0000000
--- a/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.mocks.dart
+++ /dev/null
@@ -1,246 +0,0 @@
-// Mocks generated by Mockito 5.3.0 from annotations
-// in webview_flutter/test/v4/webview_widget_test.dart.
-// Do not manually edit this file.
-
-// ignore_for_file: no_leading_underscores_for_library_prefixes
-import 'dart:async' as _i7;
-import 'dart:math' as _i3;
-import 'dart:ui' as _i9;
-
-import 'package:flutter/foundation.dart' as _i5;
-import 'package:flutter/widgets.dart' as _i4;
-import 'package:mockito/mockito.dart' as _i1;
-import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart'
-    as _i8;
-import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart'
-    as _i6;
-import 'package:webview_flutter_platform_interface/v4/src/platform_webview_widget.dart'
-    as _i10;
-import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'
-    as _i2;
-
-// 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
-// ignore_for_file: subtype_of_sealed_class
-
-class _FakePlatformWebViewControllerCreationParams_0 extends _i1.SmartFake
-    implements _i2.PlatformWebViewControllerCreationParams {
-  _FakePlatformWebViewControllerCreationParams_0(
-      Object parent, Invocation parentInvocation)
-      : super(parent, parentInvocation);
-}
-
-class _FakePoint_1<T extends num> extends _i1.SmartFake
-    implements _i3.Point<T> {
-  _FakePoint_1(Object parent, Invocation parentInvocation)
-      : super(parent, parentInvocation);
-}
-
-class _FakePlatformWebViewWidgetCreationParams_2 extends _i1.SmartFake
-    implements _i2.PlatformWebViewWidgetCreationParams {
-  _FakePlatformWebViewWidgetCreationParams_2(
-      Object parent, Invocation parentInvocation)
-      : super(parent, parentInvocation);
-}
-
-class _FakeWidget_3 extends _i1.SmartFake implements _i4.Widget {
-  _FakeWidget_3(Object parent, Invocation parentInvocation)
-      : super(parent, parentInvocation);
-
-  @override
-  String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) =>
-      super.toString();
-}
-
-/// A class which mocks [PlatformWebViewController].
-///
-/// See the documentation for Mockito's code generation for more information.
-class MockPlatformWebViewController extends _i1.Mock
-    implements _i6.PlatformWebViewController {
-  MockPlatformWebViewController() {
-    _i1.throwOnMissingStub(this);
-  }
-
-  @override
-  _i2.PlatformWebViewControllerCreationParams get params =>
-      (super.noSuchMethod(Invocation.getter(#params),
-              returnValue: _FakePlatformWebViewControllerCreationParams_0(
-                  this, Invocation.getter(#params)))
-          as _i2.PlatformWebViewControllerCreationParams);
-  @override
-  _i7.Future<void> loadFile(String? absoluteFilePath) => (super.noSuchMethod(
-      Invocation.method(#loadFile, [absoluteFilePath]),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> loadFlutterAsset(String? key) => (super.noSuchMethod(
-      Invocation.method(#loadFlutterAsset, [key]),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> loadHtmlString(String? html, {String? baseUrl}) =>
-      (super.noSuchMethod(
-              Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}),
-              returnValue: _i7.Future<void>.value(),
-              returnValueForMissingStub: _i7.Future<void>.value())
-          as _i7.Future<void>);
-  @override
-  _i7.Future<void> loadRequest(_i2.LoadRequestParams? params) =>
-      (super.noSuchMethod(Invocation.method(#loadRequest, [params]),
-              returnValue: _i7.Future<void>.value(),
-              returnValueForMissingStub: _i7.Future<void>.value())
-          as _i7.Future<void>);
-  @override
-  _i7.Future<String?> currentUrl() =>
-      (super.noSuchMethod(Invocation.method(#currentUrl, []),
-          returnValue: _i7.Future<String?>.value()) as _i7.Future<String?>);
-  @override
-  _i7.Future<bool> canGoBack() =>
-      (super.noSuchMethod(Invocation.method(#canGoBack, []),
-          returnValue: _i7.Future<bool>.value(false)) as _i7.Future<bool>);
-  @override
-  _i7.Future<bool> canGoForward() =>
-      (super.noSuchMethod(Invocation.method(#canGoForward, []),
-          returnValue: _i7.Future<bool>.value(false)) as _i7.Future<bool>);
-  @override
-  _i7.Future<void> goBack() => (super.noSuchMethod(
-      Invocation.method(#goBack, []),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> goForward() => (super.noSuchMethod(
-      Invocation.method(#goForward, []),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> reload() => (super.noSuchMethod(
-      Invocation.method(#reload, []),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> clearCache() => (super.noSuchMethod(
-      Invocation.method(#clearCache, []),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> clearLocalStorage() => (super.noSuchMethod(
-      Invocation.method(#clearLocalStorage, []),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> setPlatformNavigationDelegate(
-          _i8.PlatformNavigationDelegate? handler) =>
-      (super.noSuchMethod(
-              Invocation.method(#setPlatformNavigationDelegate, [handler]),
-              returnValue: _i7.Future<void>.value(),
-              returnValueForMissingStub: _i7.Future<void>.value())
-          as _i7.Future<void>);
-  @override
-  _i7.Future<void> runJavaScript(String? javaScript) => (super.noSuchMethod(
-      Invocation.method(#runJavaScript, [javaScript]),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<String> runJavaScriptReturningResult(String? javaScript) =>
-      (super.noSuchMethod(
-          Invocation.method(#runJavaScriptReturningResult, [javaScript]),
-          returnValue: _i7.Future<String>.value('')) as _i7.Future<String>);
-  @override
-  _i7.Future<void> addJavaScriptChannel(
-          _i6.JavaScriptChannelParams? javaScriptChannelParams) =>
-      (super.noSuchMethod(
-          Invocation.method(#addJavaScriptChannel, [javaScriptChannelParams]),
-          returnValue: _i7.Future<void>.value(),
-          returnValueForMissingStub:
-              _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> removeJavaScriptChannel(String? javaScriptChannelName) =>
-      (super.noSuchMethod(
-          Invocation.method(#removeJavaScriptChannel, [javaScriptChannelName]),
-          returnValue: _i7.Future<void>.value(),
-          returnValueForMissingStub:
-              _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<String?> getTitle() =>
-      (super.noSuchMethod(Invocation.method(#getTitle, []),
-          returnValue: _i7.Future<String?>.value()) as _i7.Future<String?>);
-  @override
-  _i7.Future<void> scrollTo(int? x, int? y) => (super.noSuchMethod(
-      Invocation.method(#scrollTo, [x, y]),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> scrollBy(int? x, int? y) => (super.noSuchMethod(
-      Invocation.method(#scrollBy, [x, y]),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<_i3.Point<int>> getScrollPosition() =>
-      (super.noSuchMethod(Invocation.method(#getScrollPosition, []),
-              returnValue: _i7.Future<_i3.Point<int>>.value(_FakePoint_1<int>(
-                  this, Invocation.method(#getScrollPosition, []))))
-          as _i7.Future<_i3.Point<int>>);
-  @override
-  _i7.Future<void> enableDebugging(bool? enabled) => (super.noSuchMethod(
-      Invocation.method(#enableDebugging, [enabled]),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> enableGestureNavigation(bool? enabled) => (super
-          .noSuchMethod(Invocation.method(#enableGestureNavigation, [enabled]),
-              returnValue: _i7.Future<void>.value(),
-              returnValueForMissingStub: _i7.Future<void>.value())
-      as _i7.Future<void>);
-  @override
-  _i7.Future<void> enableZoom(bool? enabled) => (super.noSuchMethod(
-      Invocation.method(#enableZoom, [enabled]),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> setBackgroundColor(_i9.Color? color) => (super.noSuchMethod(
-      Invocation.method(#setBackgroundColor, [color]),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-  @override
-  _i7.Future<void> setJavaScriptMode(_i2.JavaScriptMode? javaScriptMode) =>
-      (super.noSuchMethod(
-              Invocation.method(#setJavaScriptMode, [javaScriptMode]),
-              returnValue: _i7.Future<void>.value(),
-              returnValueForMissingStub: _i7.Future<void>.value())
-          as _i7.Future<void>);
-  @override
-  _i7.Future<void> setUserAgent(String? userAgent) => (super.noSuchMethod(
-      Invocation.method(#setUserAgent, [userAgent]),
-      returnValue: _i7.Future<void>.value(),
-      returnValueForMissingStub: _i7.Future<void>.value()) as _i7.Future<void>);
-}
-
-/// A class which mocks [PlatformWebViewWidget].
-///
-/// See the documentation for Mockito's code generation for more information.
-class MockPlatformWebViewWidget extends _i1.Mock
-    implements _i10.PlatformWebViewWidget {
-  MockPlatformWebViewWidget() {
-    _i1.throwOnMissingStub(this);
-  }
-
-  @override
-  _i2.PlatformWebViewWidgetCreationParams get params =>
-      (super.noSuchMethod(Invocation.getter(#params),
-              returnValue: _FakePlatformWebViewWidgetCreationParams_2(
-                  this, Invocation.getter(#params)))
-          as _i2.PlatformWebViewWidgetCreationParams);
-  @override
-  _i4.Widget build(_i4.BuildContext? context) =>
-      (super.noSuchMethod(Invocation.method(#build, [context]),
-              returnValue:
-                  _FakeWidget_3(this, Invocation.method(#build, [context])))
-          as _i4.Widget);
-}
diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.dart b/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart
similarity index 91%
rename from packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.dart
rename to packages/webview_flutter/webview_flutter/test/webview_controller_test.dart
index f767a2e..f11884b 100644
--- a/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.dart
+++ b/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart
@@ -2,19 +2,18 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:math';
 import 'dart:typed_data';
 
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
-import 'package:webview_flutter/src/v4/src/webview_controller.dart';
-import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
 
 import 'webview_controller_test.mocks.dart';
 
-@GenerateMocks(<Type>[PlatformWebViewController])
+@GenerateMocks(<Type>[PlatformWebViewController, PlatformNavigationDelegate])
 void main() {
   test('loadFile', () async {
     final MockPlatformWebViewController mockPlatformWebViewController =
@@ -286,9 +285,7 @@
     final MockPlatformWebViewController mockPlatformWebViewController =
         MockPlatformWebViewController();
     when(mockPlatformWebViewController.getScrollPosition()).thenAnswer(
-      (_) => Future<Point<int>>.value(
-        const Point<int>(2, 3),
-      ),
+      (_) => Future<Offset>.value(const Offset(2, 3)),
     );
 
     final WebViewController webViewController = WebViewController.fromPlatform(
@@ -350,4 +347,22 @@
     await webViewController.setUserAgent('userAgent');
     verify(mockPlatformWebViewController.setUserAgent('userAgent'));
   });
+
+  test('setNavigationDelegate', () async {
+    final MockPlatformWebViewController mockPlatformWebViewController =
+        MockPlatformWebViewController();
+    final WebViewController webViewController = WebViewController.fromPlatform(
+      mockPlatformWebViewController,
+    );
+
+    final MockPlatformNavigationDelegate mockPlatformNavigationDelegate =
+        MockPlatformNavigationDelegate();
+    final NavigationDelegate navigationDelegate =
+        NavigationDelegate.fromPlatform(mockPlatformNavigationDelegate);
+
+    await webViewController.setNavigationDelegate(navigationDelegate);
+    verify(mockPlatformWebViewController.setPlatformNavigationDelegate(
+      mockPlatformNavigationDelegate,
+    ));
+  });
 }
diff --git a/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart
new file mode 100644
index 0000000..2bb1ef6
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart
@@ -0,0 +1,417 @@
+// Mocks generated by Mockito 5.3.2 from annotations
+// in webview_flutter/test/webview_controller_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i5;
+import 'dart:ui' as _i3;
+
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart'
+    as _i6;
+import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart'
+    as _i4;
+import 'package:webview_flutter_platform_interface/src/webview_platform.dart'
+    as _i2;
+
+// 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
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakePlatformWebViewControllerCreationParams_0 extends _i1.SmartFake
+    implements _i2.PlatformWebViewControllerCreationParams {
+  _FakePlatformWebViewControllerCreationParams_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakeObject_1 extends _i1.SmartFake implements Object {
+  _FakeObject_1(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakeOffset_2 extends _i1.SmartFake implements _i3.Offset {
+  _FakeOffset_2(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakePlatformNavigationDelegateCreationParams_3 extends _i1.SmartFake
+    implements _i2.PlatformNavigationDelegateCreationParams {
+  _FakePlatformNavigationDelegateCreationParams_3(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+/// A class which mocks [PlatformWebViewController].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockPlatformWebViewController extends _i1.Mock
+    implements _i4.PlatformWebViewController {
+  MockPlatformWebViewController() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i2.PlatformWebViewControllerCreationParams get params => (super.noSuchMethod(
+        Invocation.getter(#params),
+        returnValue: _FakePlatformWebViewControllerCreationParams_0(
+          this,
+          Invocation.getter(#params),
+        ),
+      ) as _i2.PlatformWebViewControllerCreationParams);
+  @override
+  _i5.Future<void> loadFile(String? absoluteFilePath) => (super.noSuchMethod(
+        Invocation.method(
+          #loadFile,
+          [absoluteFilePath],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> loadFlutterAsset(String? key) => (super.noSuchMethod(
+        Invocation.method(
+          #loadFlutterAsset,
+          [key],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> loadHtmlString(
+    String? html, {
+    String? baseUrl,
+  }) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #loadHtmlString,
+          [html],
+          {#baseUrl: baseUrl},
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> loadRequest(_i2.LoadRequestParams? params) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #loadRequest,
+          [params],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<String?> currentUrl() => (super.noSuchMethod(
+        Invocation.method(
+          #currentUrl,
+          [],
+        ),
+        returnValue: _i5.Future<String?>.value(),
+      ) as _i5.Future<String?>);
+  @override
+  _i5.Future<bool> canGoBack() => (super.noSuchMethod(
+        Invocation.method(
+          #canGoBack,
+          [],
+        ),
+        returnValue: _i5.Future<bool>.value(false),
+      ) as _i5.Future<bool>);
+  @override
+  _i5.Future<bool> canGoForward() => (super.noSuchMethod(
+        Invocation.method(
+          #canGoForward,
+          [],
+        ),
+        returnValue: _i5.Future<bool>.value(false),
+      ) as _i5.Future<bool>);
+  @override
+  _i5.Future<void> goBack() => (super.noSuchMethod(
+        Invocation.method(
+          #goBack,
+          [],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> goForward() => (super.noSuchMethod(
+        Invocation.method(
+          #goForward,
+          [],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> reload() => (super.noSuchMethod(
+        Invocation.method(
+          #reload,
+          [],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> clearCache() => (super.noSuchMethod(
+        Invocation.method(
+          #clearCache,
+          [],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> clearLocalStorage() => (super.noSuchMethod(
+        Invocation.method(
+          #clearLocalStorage,
+          [],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setPlatformNavigationDelegate(
+          _i6.PlatformNavigationDelegate? handler) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setPlatformNavigationDelegate,
+          [handler],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> runJavaScript(String? javaScript) => (super.noSuchMethod(
+        Invocation.method(
+          #runJavaScript,
+          [javaScript],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<Object> runJavaScriptReturningResult(String? javaScript) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #runJavaScriptReturningResult,
+          [javaScript],
+        ),
+        returnValue: _i5.Future<Object>.value(_FakeObject_1(
+          this,
+          Invocation.method(
+            #runJavaScriptReturningResult,
+            [javaScript],
+          ),
+        )),
+      ) as _i5.Future<Object>);
+  @override
+  _i5.Future<void> addJavaScriptChannel(
+          _i4.JavaScriptChannelParams? javaScriptChannelParams) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #addJavaScriptChannel,
+          [javaScriptChannelParams],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> removeJavaScriptChannel(String? javaScriptChannelName) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #removeJavaScriptChannel,
+          [javaScriptChannelName],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<String?> getTitle() => (super.noSuchMethod(
+        Invocation.method(
+          #getTitle,
+          [],
+        ),
+        returnValue: _i5.Future<String?>.value(),
+      ) as _i5.Future<String?>);
+  @override
+  _i5.Future<void> scrollTo(
+    int? x,
+    int? y,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #scrollTo,
+          [
+            x,
+            y,
+          ],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> scrollBy(
+    int? x,
+    int? y,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #scrollBy,
+          [
+            x,
+            y,
+          ],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<_i3.Offset> getScrollPosition() => (super.noSuchMethod(
+        Invocation.method(
+          #getScrollPosition,
+          [],
+        ),
+        returnValue: _i5.Future<_i3.Offset>.value(_FakeOffset_2(
+          this,
+          Invocation.method(
+            #getScrollPosition,
+            [],
+          ),
+        )),
+      ) as _i5.Future<_i3.Offset>);
+  @override
+  _i5.Future<void> enableZoom(bool? enabled) => (super.noSuchMethod(
+        Invocation.method(
+          #enableZoom,
+          [enabled],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setBackgroundColor(_i3.Color? color) => (super.noSuchMethod(
+        Invocation.method(
+          #setBackgroundColor,
+          [color],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setJavaScriptMode(_i2.JavaScriptMode? javaScriptMode) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setJavaScriptMode,
+          [javaScriptMode],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setUserAgent(String? userAgent) => (super.noSuchMethod(
+        Invocation.method(
+          #setUserAgent,
+          [userAgent],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+}
+
+/// A class which mocks [PlatformNavigationDelegate].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockPlatformNavigationDelegate extends _i1.Mock
+    implements _i6.PlatformNavigationDelegate {
+  MockPlatformNavigationDelegate() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i2.PlatformNavigationDelegateCreationParams get params =>
+      (super.noSuchMethod(
+        Invocation.getter(#params),
+        returnValue: _FakePlatformNavigationDelegateCreationParams_3(
+          this,
+          Invocation.getter(#params),
+        ),
+      ) as _i2.PlatformNavigationDelegateCreationParams);
+  @override
+  _i5.Future<void> setOnNavigationRequest(
+          _i6.NavigationRequestCallback? onNavigationRequest) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnNavigationRequest,
+          [onNavigationRequest],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setOnPageStarted(_i6.PageEventCallback? onPageStarted) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnPageStarted,
+          [onPageStarted],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setOnPageFinished(_i6.PageEventCallback? onPageFinished) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnPageFinished,
+          [onPageFinished],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setOnProgress(_i6.ProgressCallback? onProgress) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnProgress,
+          [onProgress],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+  @override
+  _i5.Future<void> setOnWebResourceError(
+          _i6.WebResourceErrorCallback? onWebResourceError) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setOnWebResourceError,
+          [onWebResourceError],
+        ),
+        returnValue: _i5.Future<void>.value(),
+        returnValueForMissingStub: _i5.Future<void>.value(),
+      ) as _i5.Future<void>);
+}
diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.dart
similarity index 90%
rename from packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.dart
rename to packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.dart
index e815240..babf74b 100644
--- a/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.dart
+++ b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.dart
@@ -5,8 +5,8 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
-import 'package:webview_flutter/src/v4/src/webview_cookie_manager.dart';
-import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
 
 import 'webview_cookie_manager_test.mocks.dart';
 
diff --git a/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart
new file mode 100644
index 0000000..7cae663
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart
@@ -0,0 +1,71 @@
+// Mocks generated by Mockito 5.3.2 from annotations
+// in webview_flutter/test/webview_cookie_manager_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i4;
+
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:webview_flutter_platform_interface/src/platform_webview_cookie_manager.dart'
+    as _i3;
+import 'package:webview_flutter_platform_interface/src/webview_platform.dart'
+    as _i2;
+
+// 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
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakePlatformWebViewCookieManagerCreationParams_0 extends _i1.SmartFake
+    implements _i2.PlatformWebViewCookieManagerCreationParams {
+  _FakePlatformWebViewCookieManagerCreationParams_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+/// A class which mocks [PlatformWebViewCookieManager].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockPlatformWebViewCookieManager extends _i1.Mock
+    implements _i3.PlatformWebViewCookieManager {
+  MockPlatformWebViewCookieManager() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i2.PlatformWebViewCookieManagerCreationParams get params =>
+      (super.noSuchMethod(
+        Invocation.getter(#params),
+        returnValue: _FakePlatformWebViewCookieManagerCreationParams_0(
+          this,
+          Invocation.getter(#params),
+        ),
+      ) as _i2.PlatformWebViewCookieManagerCreationParams);
+  @override
+  _i4.Future<bool> clearCookies() => (super.noSuchMethod(
+        Invocation.method(
+          #clearCookies,
+          [],
+        ),
+        returnValue: _i4.Future<bool>.value(false),
+      ) as _i4.Future<bool>);
+  @override
+  _i4.Future<void> setCookie(_i2.WebViewCookie? cookie) => (super.noSuchMethod(
+        Invocation.method(
+          #setCookie,
+          [cookie],
+        ),
+        returnValue: _i4.Future<void>.value(),
+        returnValueForMissingStub: _i4.Future<void>.value(),
+      ) as _i4.Future<void>);
+}
diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart
index b10366c..d13b51c 100644
--- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart
+++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart
@@ -2,1366 +2,43 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:typed_data';
-
-import 'package:flutter/src/foundation/basic_types.dart';
-import 'package:flutter/src/gestures/recognizer.dart';
-import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:mockito/annotations.dart';
-import 'package:mockito/mockito.dart';
-import 'package:webview_flutter/webview_flutter.dart';
-import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+import 'package:webview_flutter/webview_flutter.dart' as main_file;
 
-import 'webview_flutter_test.mocks.dart';
-
-typedef VoidCallback = void Function();
-
-@GenerateMocks(<Type>[WebViewPlatform, WebViewPlatformController])
 void main() {
-  TestWidgetsFlutterBinding.ensureInitialized();
-
-  late MockWebViewPlatform mockWebViewPlatform;
-  late MockWebViewPlatformController mockWebViewPlatformController;
-  late MockWebViewCookieManagerPlatform mockWebViewCookieManagerPlatform;
-
-  setUp(() {
-    mockWebViewPlatformController = MockWebViewPlatformController();
-    mockWebViewPlatform = MockWebViewPlatform();
-    mockWebViewCookieManagerPlatform = MockWebViewCookieManagerPlatform();
-    when(mockWebViewPlatform.build(
-      context: anyNamed('context'),
-      creationParams: anyNamed('creationParams'),
-      webViewPlatformCallbacksHandler:
-          anyNamed('webViewPlatformCallbacksHandler'),
-      javascriptChannelRegistry: anyNamed('javascriptChannelRegistry'),
-      onWebViewPlatformCreated: anyNamed('onWebViewPlatformCreated'),
-      gestureRecognizers: anyNamed('gestureRecognizers'),
-    )).thenAnswer((Invocation invocation) {
-      final WebViewPlatformCreatedCallback onWebViewPlatformCreated =
-          invocation.namedArguments[const Symbol('onWebViewPlatformCreated')]
-              as WebViewPlatformCreatedCallback;
-      return TestPlatformWebView(
-        mockWebViewPlatformController: mockWebViewPlatformController,
-        onWebViewPlatformCreated: onWebViewPlatformCreated,
-      );
-    });
-
-    WebView.platform = mockWebViewPlatform;
-    WebViewCookieManagerPlatform.instance = mockWebViewCookieManagerPlatform;
-  });
-
-  tearDown(() {
-    mockWebViewCookieManagerPlatform.reset();
-  });
-
-  testWidgets('Create WebView', (WidgetTester tester) async {
-    await tester.pumpWidget(const WebView());
-  });
-
-  testWidgets('Initial url', (WidgetTester tester) async {
-    await tester.pumpWidget(const WebView(initialUrl: 'https://youtube.com'));
-
-    final CreationParams params = captureBuildArgs(
-      mockWebViewPlatform,
-      creationParams: true,
-    ).single as CreationParams;
-
-    expect(params.initialUrl, 'https://youtube.com');
-  });
-
-  testWidgets('Javascript mode', (WidgetTester tester) async {
-    await tester.pumpWidget(const WebView(
-      javascriptMode: JavascriptMode.unrestricted,
-    ));
-
-    final CreationParams unrestrictedparams = captureBuildArgs(
-      mockWebViewPlatform,
-      creationParams: true,
-    ).single as CreationParams;
-
-    expect(
-      unrestrictedparams.webSettings!.javascriptMode,
-      JavascriptMode.unrestricted,
-    );
-
-    await tester.pumpWidget(const WebView());
-
-    final CreationParams disabledparams = captureBuildArgs(
-      mockWebViewPlatform,
-      creationParams: true,
-    ).single as CreationParams;
-
-    expect(disabledparams.webSettings!.javascriptMode, JavascriptMode.disabled);
-  });
-
-  testWidgets('Load file', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    await controller!.loadFile('/test/path/index.html');
-
-    verify(mockWebViewPlatformController.loadFile(
-      '/test/path/index.html',
-    ));
-  });
-
-  testWidgets('Load file with empty path', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    expect(() => controller!.loadFile(''), throwsAssertionError);
-  });
-
-  testWidgets('Load Flutter asset', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    await controller!.loadFlutterAsset('assets/index.html');
-
-    verify(mockWebViewPlatformController.loadFlutterAsset(
-      'assets/index.html',
-    ));
-  });
-
-  testWidgets('Load Flutter asset with empty key', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    expect(() => controller!.loadFlutterAsset(''), throwsAssertionError);
-  });
-
-  testWidgets('Load HTML string without base URL', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    await controller!.loadHtmlString('<p>This is a test paragraph.</p>');
-
-    verify(mockWebViewPlatformController.loadHtmlString(
-      '<p>This is a test paragraph.</p>',
-    ));
-  });
-
-  testWidgets('Load HTML string with base URL', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    await controller!.loadHtmlString(
-      '<p>This is a test paragraph.</p>',
-      baseUrl: 'https://flutter.dev',
-    );
-
-    verify(mockWebViewPlatformController.loadHtmlString(
-      '<p>This is a test paragraph.</p>',
-      baseUrl: 'https://flutter.dev',
-    ));
-  });
-
-  testWidgets('Load HTML string with empty string',
-      (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    expect(() => controller!.loadHtmlString(''), throwsAssertionError);
-  });
-
-  testWidgets('Load url', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    await controller!.loadUrl('https://flutter.io');
-
-    verify(mockWebViewPlatformController.loadUrl(
-      'https://flutter.io',
-      argThat(isNull),
-    ));
-  });
-
-  testWidgets('Invalid urls', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    final CreationParams params = captureBuildArgs(
-      mockWebViewPlatform,
-      creationParams: true,
-    ).single as CreationParams;
-
-    expect(params.initialUrl, isNull);
-
-    expect(() => controller!.loadUrl(''), throwsA(anything));
-    expect(() => controller!.loadUrl('flutter.io'), throwsA(anything));
-  });
-
-  testWidgets('Headers in loadUrl', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    final Map<String, String> headers = <String, String>{
-      'CACHE-CONTROL': 'ABC'
-    };
-    await controller!.loadUrl('https://flutter.io', headers: headers);
-
-    verify(mockWebViewPlatformController.loadUrl(
-      'https://flutter.io',
-      <String, String>{'CACHE-CONTROL': 'ABC'},
-    ));
-  });
-
-  testWidgets('loadRequest', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-    expect(controller, isNotNull);
-
-    final WebViewRequest req = WebViewRequest(
-      uri: Uri.parse('https://flutter.dev'),
-      method: WebViewRequestMethod.post,
-      headers: <String, String>{'foo': 'bar'},
-      body: Uint8List.fromList('Test Body'.codeUnits),
-    );
-
-    await controller!.loadRequest(req);
-
-    verify(mockWebViewPlatformController.loadRequest(req));
-  });
-
-  testWidgets('Clear Cache', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-
-    await controller!.clearCache();
-
-    verify(mockWebViewPlatformController.clearCache());
-  });
-
-  testWidgets('Can go back', (WidgetTester tester) async {
-    when(mockWebViewPlatformController.canGoBack())
-        .thenAnswer((_) => Future<bool>.value(true));
-
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://flutter.io',
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-    expect(controller!.canGoBack(), completion(true));
-  });
-
-  testWidgets("Can't go forward", (WidgetTester tester) async {
-    when(mockWebViewPlatformController.canGoForward())
-        .thenAnswer((_) => Future<bool>.value(false));
-
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-    expect(controller!.canGoForward(), completion(false));
-  });
-
-  testWidgets('Go back', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://youtube.com',
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    await controller!.goBack();
-    verify(mockWebViewPlatformController.goBack());
-  });
-
-  testWidgets('Go forward', (WidgetTester tester) async {
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://youtube.com',
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-    await controller!.goForward();
-    verify(mockWebViewPlatformController.goForward());
-  });
-
-  testWidgets('Current URL', (WidgetTester tester) async {
-    when(mockWebViewPlatformController.currentUrl())
-        .thenAnswer((_) => Future<String>.value('https://youtube.com'));
-
-    WebViewController? controller;
-    await tester.pumpWidget(
-      WebView(
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(controller, isNotNull);
-    expect(await controller!.currentUrl(), 'https://youtube.com');
-  });
-
-  testWidgets('Reload url', (WidgetTester tester) async {
-    late WebViewController controller;
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://flutter.io',
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    await controller.reload();
-    verify(mockWebViewPlatformController.reload());
-  });
-
-  testWidgets('evaluate Javascript', (WidgetTester tester) async {
-    when(mockWebViewPlatformController.evaluateJavascript('fake js string'))
-        .thenAnswer((_) => Future<String>.value('fake js string'));
-
-    late WebViewController controller;
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://flutter.io',
-        javascriptMode: JavascriptMode.unrestricted,
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-
-    expect(
-        // ignore: deprecated_member_use_from_same_package
-        await controller.evaluateJavascript('fake js string'),
-        'fake js string',
-        reason: 'should get the argument');
-  });
-
-  testWidgets('evaluate Javascript with JavascriptMode disabled',
-      (WidgetTester tester) async {
-    late WebViewController controller;
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://flutter.io',
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-    expect(
-      // ignore: deprecated_member_use_from_same_package
-      () => controller.evaluateJavascript('fake js string'),
-      throwsA(anything),
-    );
-  });
-
-  testWidgets('runJavaScript', (WidgetTester tester) async {
-    late WebViewController controller;
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://flutter.io',
-        javascriptMode: JavascriptMode.unrestricted,
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-    await controller.runJavascript('fake js string');
-    verify(mockWebViewPlatformController.runJavascript('fake js string'));
-  });
-
-  testWidgets('runJavaScript with JavascriptMode disabled',
-      (WidgetTester tester) async {
-    late WebViewController controller;
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://flutter.io',
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-    expect(
-      () => controller.runJavascript('fake js string'),
-      throwsA(anything),
-    );
-  });
-
-  testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async {
-    when(mockWebViewPlatformController
-            .runJavascriptReturningResult('fake js string'))
-        .thenAnswer((_) => Future<String>.value('fake js string'));
-
-    late WebViewController controller;
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://flutter.io',
-        javascriptMode: JavascriptMode.unrestricted,
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-    expect(await controller.runJavascriptReturningResult('fake js string'),
-        'fake js string',
-        reason: 'should get the argument');
-  });
-
-  testWidgets('runJavaScriptReturningResult with JavascriptMode disabled',
-      (WidgetTester tester) async {
-    late WebViewController controller;
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://flutter.io',
-        onWebViewCreated: (WebViewController webViewController) {
-          controller = webViewController;
-        },
-      ),
-    );
-    expect(
-      () => controller.runJavascriptReturningResult('fake js string'),
-      throwsA(anything),
-    );
-  });
-
-  testWidgets('Cookies can be cleared once', (WidgetTester tester) async {
-    await tester.pumpWidget(
-      const WebView(
-        initialUrl: 'https://flutter.io',
-      ),
-    );
-    final CookieManager cookieManager = CookieManager();
-    final bool hasCookies = await cookieManager.clearCookies();
-    expect(hasCookies, true);
-  });
-
-  testWidgets('Cookies can be set', (WidgetTester tester) async {
-    const WebViewCookie cookie =
-        WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev');
-
-    await tester.pumpWidget(
-      const WebView(
-        initialUrl: 'https://flutter.io',
-      ),
-    );
-    final CookieManager cookieManager = CookieManager();
-    await cookieManager.setCookie(cookie);
-    expect(mockWebViewCookieManagerPlatform.setCookieCalls,
-        <WebViewCookie>[cookie]);
-  });
-
-  testWidgets('Initial JavaScript channels', (WidgetTester tester) async {
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://youtube.com',
-        javascriptChannels: <JavascriptChannel>{
-          JavascriptChannel(
-              name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
-          JavascriptChannel(
-              name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
-        },
-      ),
-    );
-
-    final CreationParams params = captureBuildArgs(
-      mockWebViewPlatform,
-      creationParams: true,
-    ).single as CreationParams;
-
-    expect(params.javascriptChannelNames,
-        unorderedEquals(<String>['Tts', 'Alarm']));
-  });
-
-  test('Only valid JavaScript channel names are allowed', () {
-    void noOp(JavascriptMessage msg) {}
-    JavascriptChannel(name: 'Tts1', onMessageReceived: noOp);
-    JavascriptChannel(name: '_Alarm', onMessageReceived: noOp);
-    JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp);
-
-    VoidCallback createChannel(String name) {
-      return () {
-        JavascriptChannel(name: name, onMessageReceived: noOp);
-      };
-    }
-
-    expect(createChannel('1Alarm'), throwsAssertionError);
-    expect(createChannel('foo.bar'), throwsAssertionError);
-    expect(createChannel(''), throwsAssertionError);
-  });
-
-  testWidgets('Unique JavaScript channel names are required',
-      (WidgetTester tester) async {
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://youtube.com',
-        javascriptChannels: <JavascriptChannel>{
-          JavascriptChannel(
-              name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
-          JavascriptChannel(
-              name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
-        },
-      ),
-    );
-    expect(tester.takeException(), isNot(null));
-  });
-
-  testWidgets('JavaScript channels update', (WidgetTester tester) async {
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://youtube.com',
-        javascriptChannels: <JavascriptChannel>{
-          JavascriptChannel(
-              name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
-          JavascriptChannel(
-              name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}),
-        },
-      ),
-    );
-
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://youtube.com',
-        javascriptChannels: <JavascriptChannel>{
-          JavascriptChannel(
-              name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
-          JavascriptChannel(
-              name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}),
-          JavascriptChannel(
-              name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}),
-        },
-      ),
-    );
-
-    final JavascriptChannelRegistry channelRegistry = captureBuildArgs(
-      mockWebViewPlatform,
-      javascriptChannelRegistry: true,
-    ).first as JavascriptChannelRegistry;
-
-    expect(
-      channelRegistry.channels.keys,
-      unorderedEquals(<String>['Tts', 'Alarm2', 'Alarm3']),
-    );
-  });
-
-  testWidgets('Remove all JavaScript channels and then add',
-      (WidgetTester tester) async {
-    // This covers a specific bug we had where after updating javascriptChannels to null,
-    // updating it again with a subset of the previously registered channels fails as the
-    // widget's cache of current channel wasn't properly updated when updating javascriptChannels to
-    // null.
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://youtube.com',
-        javascriptChannels: <JavascriptChannel>{
-          JavascriptChannel(
-              name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
-        },
-      ),
-    );
-
-    await tester.pumpWidget(
-      const WebView(
-        initialUrl: 'https://youtube.com',
-      ),
-    );
-
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://youtube.com',
-        javascriptChannels: <JavascriptChannel>{
-          JavascriptChannel(
-              name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}),
-        },
-      ),
-    );
-
-    final JavascriptChannelRegistry channelRegistry = captureBuildArgs(
-      mockWebViewPlatform,
-      javascriptChannelRegistry: true,
-    ).last as JavascriptChannelRegistry;
-
-    expect(channelRegistry.channels.keys, unorderedEquals(<String>['Tts']));
-  });
-
-  testWidgets('JavaScript channel messages', (WidgetTester tester) async {
-    final List<String> ttsMessagesReceived = <String>[];
-    final List<String> alarmMessagesReceived = <String>[];
-    await tester.pumpWidget(
-      WebView(
-        initialUrl: 'https://youtube.com',
-        javascriptChannels: <JavascriptChannel>{
-          JavascriptChannel(
-              name: 'Tts',
-              onMessageReceived: (JavascriptMessage msg) {
-                ttsMessagesReceived.add(msg.message);
-              }),
-          JavascriptChannel(
-              name: 'Alarm',
-              onMessageReceived: (JavascriptMessage msg) {
-                alarmMessagesReceived.add(msg.message);
-              }),
-        },
-      ),
-    );
-
-    final JavascriptChannelRegistry channelRegistry = captureBuildArgs(
-      mockWebViewPlatform,
-      javascriptChannelRegistry: true,
-    ).single as JavascriptChannelRegistry;
-
-    expect(ttsMessagesReceived, isEmpty);
-    expect(alarmMessagesReceived, isEmpty);
-
-    channelRegistry.onJavascriptChannelMessage('Tts', 'Hello');
-    channelRegistry.onJavascriptChannelMessage('Tts', 'World');
-
-    expect(ttsMessagesReceived, <String>['Hello', 'World']);
-  });
-
-  group('$PageStartedCallback', () {
-    testWidgets('onPageStarted is not null', (WidgetTester tester) async {
-      String? returnedUrl;
-
-      await tester.pumpWidget(WebView(
-        initialUrl: 'https://youtube.com',
-        onPageStarted: (String url) {
-          returnedUrl = url;
-        },
-      ));
-
-      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
-        mockWebViewPlatform,
-        webViewPlatformCallbacksHandler: true,
-      ).single as WebViewPlatformCallbacksHandler;
-
-      handler.onPageStarted('https://youtube.com');
-
-      expect(returnedUrl, 'https://youtube.com');
-    });
-
-    testWidgets('onPageStarted is null', (WidgetTester tester) async {
-      await tester.pumpWidget(const WebView(
-        initialUrl: 'https://youtube.com',
-      ));
-
-      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
-        mockWebViewPlatform,
-        webViewPlatformCallbacksHandler: true,
-      ).single as WebViewPlatformCallbacksHandler;
-
-      // The platform side will always invoke a call for onPageStarted. This is
-      // to test that it does not crash on a null callback.
-      handler.onPageStarted('https://youtube.com');
-    });
-
-    testWidgets('onPageStarted changed', (WidgetTester tester) async {
-      String? returnedUrl;
-
-      await tester.pumpWidget(WebView(
-        initialUrl: 'https://youtube.com',
-        onPageStarted: (String url) {},
-      ));
-
-      await tester.pumpWidget(WebView(
-        initialUrl: 'https://youtube.com',
-        onPageStarted: (String url) {
-          returnedUrl = url;
-        },
-      ));
-
-      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
-        mockWebViewPlatform,
-        webViewPlatformCallbacksHandler: true,
-      ).last as WebViewPlatformCallbacksHandler;
-      handler.onPageStarted('https://youtube.com');
-
-      expect(returnedUrl, 'https://youtube.com');
+  group('webview_flutter', () {
+    test('ensure webview_flutter.dart exports classes from platform interface',
+        () {
+      // ignore: unnecessary_statements
+      main_file.JavaScriptMessage;
+      // ignore: unnecessary_statements
+      main_file.JavaScriptMode;
+      // ignore: unnecessary_statements
+      main_file.LoadRequestMethod;
+      // ignore: unnecessary_statements
+      main_file.NavigationDecision;
+      // ignore: unnecessary_statements
+      main_file.NavigationRequest;
+      // ignore: unnecessary_statements
+      main_file.NavigationRequestCallback;
+      // ignore: unnecessary_statements
+      main_file.PageEventCallback;
+      // ignore: unnecessary_statements
+      main_file.PlatformNavigationDelegateCreationParams;
+      // ignore: unnecessary_statements
+      main_file.PlatformWebViewControllerCreationParams;
+      // ignore: unnecessary_statements
+      main_file.PlatformWebViewCookieManagerCreationParams;
+      // ignore: unnecessary_statements
+      main_file.PlatformWebViewWidgetCreationParams;
+      // ignore: unnecessary_statements
+      main_file.ProgressCallback;
+      // ignore: unnecessary_statements
+      main_file.WebResourceError;
+      // ignore: unnecessary_statements
+      main_file.WebResourceErrorCallback;
+      // ignore: unnecessary_statements
+      main_file.WebViewCookie;
     });
   });
-
-  group('$PageFinishedCallback', () {
-    testWidgets('onPageFinished is not null', (WidgetTester tester) async {
-      String? returnedUrl;
-
-      await tester.pumpWidget(WebView(
-        initialUrl: 'https://youtube.com',
-        onPageFinished: (String url) {
-          returnedUrl = url;
-        },
-      ));
-
-      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
-        mockWebViewPlatform,
-        webViewPlatformCallbacksHandler: true,
-      ).single as WebViewPlatformCallbacksHandler;
-      handler.onPageFinished('https://youtube.com');
-
-      expect(returnedUrl, 'https://youtube.com');
-    });
-
-    testWidgets('onPageFinished is null', (WidgetTester tester) async {
-      await tester.pumpWidget(const WebView(
-        initialUrl: 'https://youtube.com',
-      ));
-
-      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
-        mockWebViewPlatform,
-        webViewPlatformCallbacksHandler: true,
-      ).single as WebViewPlatformCallbacksHandler;
-      // The platform side will always invoke a call for onPageFinished. This is
-      // to test that it does not crash on a null callback.
-      handler.onPageFinished('https://youtube.com');
-    });
-
-    testWidgets('onPageFinished changed', (WidgetTester tester) async {
-      String? returnedUrl;
-
-      await tester.pumpWidget(WebView(
-        initialUrl: 'https://youtube.com',
-        onPageFinished: (String url) {},
-      ));
-
-      await tester.pumpWidget(WebView(
-        initialUrl: 'https://youtube.com',
-        onPageFinished: (String url) {
-          returnedUrl = url;
-        },
-      ));
-
-      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
-        mockWebViewPlatform,
-        webViewPlatformCallbacksHandler: true,
-      ).last as WebViewPlatformCallbacksHandler;
-      handler.onPageFinished('https://youtube.com');
-
-      expect(returnedUrl, 'https://youtube.com');
-    });
-  });
-
-  group('$PageLoadingCallback', () {
-    testWidgets('onLoadingProgress is not null', (WidgetTester tester) async {
-      int? loadingProgress;
-
-      await tester.pumpWidget(WebView(
-        initialUrl: 'https://youtube.com',
-        onProgress: (int progress) {
-          loadingProgress = progress;
-        },
-      ));
-
-      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
-        mockWebViewPlatform,
-        webViewPlatformCallbacksHandler: true,
-      ).single as WebViewPlatformCallbacksHandler;
-      handler.onProgress(50);
-
-      expect(loadingProgress, 50);
-    });
-
-    testWidgets('onLoadingProgress is null', (WidgetTester tester) async {
-      await tester.pumpWidget(const WebView(
-        initialUrl: 'https://youtube.com',
-      ));
-
-      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
-        mockWebViewPlatform,
-        webViewPlatformCallbacksHandler: true,
-      ).single as WebViewPlatformCallbacksHandler;
-
-      // This is to test that it does not crash on a null callback.
-      handler.onProgress(50);
-    });
-
-    testWidgets('onLoadingProgress changed', (WidgetTester tester) async {
-      int? loadingProgress;
-
-      await tester.pumpWidget(WebView(
-        initialUrl: 'https://youtube.com',
-        onProgress: (int progress) {},
-      ));
-
-      await tester.pumpWidget(WebView(
-        initialUrl: 'https://youtube.com',
-        onProgress: (int progress) {
-          loadingProgress = progress;
-        },
-      ));
-
-      final WebViewPlatformCallbacksHandler handler = captureBuildArgs(
-        mockWebViewPlatform,
-        webViewPlatformCallbacksHandler: true,
-      ).last as WebViewPlatformCallbacksHandler;
-      handler.onProgress(50);
-
-      expect(loadingProgress, 50);
-    });
-  });
-
-  group('navigationDelegate', () {
-    testWidgets('hasNavigationDelegate', (WidgetTester tester) async {
-      await tester.pumpWidget(const WebView(
-        initialUrl: 'https://youtube.com',
-      ));
-
-      final CreationParams params = captureBuildArgs(
-        mockWebViewPlatform,
-        creationParams: true,
-      ).single as CreationParams;
-
-      expect(params.webSettings!.hasNavigationDelegate, false);
-
-      await tester.pumpWidget(WebView(
-        initialUrl: 'https://youtube.com',
-        navigationDelegate: (NavigationRequest r) =>
-            NavigationDecision.navigate,
-      ));
-
-      final WebSettings updateSettings =
-          verify(mockWebViewPlatformController.updateSettings(captureAny))
-              .captured
-              .single as WebSettings;
-
-      expect(updateSettings.hasNavigationDelegate, true);
-    });
-
-    testWidgets('Block navigation', (WidgetTester tester) async {
-      final List<NavigationRequest> navigationRequests = <NavigationRequest>[];
-
-      await tester.pumpWidget(WebView(
-          initialUrl: 'https://youtube.com',
-          navigationDelegate: (NavigationRequest request) {
-            navigationRequests.add(request);
-            // Only allow navigating to https://flutter.dev
-            return request.url == 'https://flutter.dev'
-                ? NavigationDecision.navigate
-                : NavigationDecision.prevent;
-          }));
-
-      final List<dynamic> args = captureBuildArgs(
-        mockWebViewPlatform,
-        creationParams: true,
-        webViewPlatformCallbacksHandler: true,
-      );
-
-      final CreationParams params = args[0] as CreationParams;
-      expect(params.webSettings!.hasNavigationDelegate, true);
-
-      final WebViewPlatformCallbacksHandler handler =
-          args[1] as WebViewPlatformCallbacksHandler;
-
-      // The navigation delegate only allows navigation to https://flutter.dev
-      // so we should still be in https://youtube.com.
-      expect(
-        handler.onNavigationRequest(
-          url: 'https://www.google.com',
-          isForMainFrame: true,
-        ),
-        completion(false),
-      );
-
-      expect(navigationRequests.length, 1);
-      expect(navigationRequests[0].url, 'https://www.google.com');
-      expect(navigationRequests[0].isForMainFrame, true);
-
-      expect(
-        handler.onNavigationRequest(
-          url: 'https://flutter.dev',
-          isForMainFrame: true,
-        ),
-        completion(true),
-      );
-    });
-  });
-
-  group('debuggingEnabled', () {
-    testWidgets('enable debugging', (WidgetTester tester) async {
-      await tester.pumpWidget(const WebView(
-        debuggingEnabled: true,
-      ));
-
-      final CreationParams params = captureBuildArgs(
-        mockWebViewPlatform,
-        creationParams: true,
-      ).single as CreationParams;
-
-      expect(params.webSettings!.debuggingEnabled, true);
-    });
-
-    testWidgets('defaults to false', (WidgetTester tester) async {
-      await tester.pumpWidget(const WebView());
-
-      final CreationParams params = captureBuildArgs(
-        mockWebViewPlatform,
-        creationParams: true,
-      ).single as CreationParams;
-
-      expect(params.webSettings!.debuggingEnabled, false);
-    });
-
-    testWidgets('can be changed', (WidgetTester tester) async {
-      final GlobalKey key = GlobalKey();
-      await tester.pumpWidget(WebView(key: key));
-
-      await tester.pumpWidget(WebView(
-        key: key,
-        debuggingEnabled: true,
-      ));
-
-      final WebSettings enabledSettings =
-          verify(mockWebViewPlatformController.updateSettings(captureAny))
-              .captured
-              .last as WebSettings;
-      expect(enabledSettings.debuggingEnabled, true);
-
-      await tester.pumpWidget(WebView(
-        key: key,
-      ));
-
-      final WebSettings disabledSettings =
-          verify(mockWebViewPlatformController.updateSettings(captureAny))
-              .captured
-              .last as WebSettings;
-      expect(disabledSettings.debuggingEnabled, false);
-    });
-  });
-
-  group('zoomEnabled', () {
-    testWidgets('Enable zoom', (WidgetTester tester) async {
-      await tester.pumpWidget(const WebView());
-
-      final CreationParams params = captureBuildArgs(
-        mockWebViewPlatform,
-        creationParams: true,
-      ).single as CreationParams;
-
-      expect(params.webSettings!.zoomEnabled, isTrue);
-    });
-
-    testWidgets('defaults to true', (WidgetTester tester) async {
-      await tester.pumpWidget(const WebView());
-
-      final CreationParams params = captureBuildArgs(
-        mockWebViewPlatform,
-        creationParams: true,
-      ).single as CreationParams;
-
-      expect(params.webSettings!.zoomEnabled, isTrue);
-    });
-
-    testWidgets('can be changed', (WidgetTester tester) async {
-      final GlobalKey key = GlobalKey();
-      await tester.pumpWidget(WebView(key: key));
-
-      await tester.pumpWidget(WebView(
-        key: key,
-      ));
-
-      final WebSettings enabledSettings =
-          verify(mockWebViewPlatformController.updateSettings(captureAny))
-              .captured
-              .last as WebSettings;
-      // Zoom defaults to true, so no changes are made to settings.
-      expect(enabledSettings.zoomEnabled, isNull);
-
-      await tester.pumpWidget(WebView(
-        key: key,
-        zoomEnabled: false,
-      ));
-
-      final WebSettings disabledSettings =
-          verify(mockWebViewPlatformController.updateSettings(captureAny))
-              .captured
-              .last as WebSettings;
-      expect(disabledSettings.zoomEnabled, isFalse);
-    });
-  });
-
-  group('Background color', () {
-    testWidgets('Defaults to null', (WidgetTester tester) async {
-      await tester.pumpWidget(const WebView());
-
-      final CreationParams params = captureBuildArgs(
-        mockWebViewPlatform,
-        creationParams: true,
-      ).single as CreationParams;
-
-      expect(params.backgroundColor, null);
-    });
-
-    testWidgets('Can be transparent', (WidgetTester tester) async {
-      const Color transparentColor = Color(0x00000000);
-
-      await tester.pumpWidget(const WebView(
-        backgroundColor: transparentColor,
-      ));
-
-      final CreationParams params = captureBuildArgs(
-        mockWebViewPlatform,
-        creationParams: true,
-      ).single as CreationParams;
-
-      expect(params.backgroundColor, transparentColor);
-    });
-  });
-
-  group('Custom platform implementation', () {
-    setUp(() {
-      WebView.platform = MyWebViewPlatform();
-    });
-    tearDownAll(() {
-      WebView.platform = null;
-    });
-
-    testWidgets('creation', (WidgetTester tester) async {
-      await tester.pumpWidget(
-        const WebView(
-          initialUrl: 'https://youtube.com',
-          gestureNavigationEnabled: true,
-        ),
-      );
-
-      final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform;
-      final MyWebViewPlatformController platform = builder.lastPlatformBuilt!;
-
-      expect(
-          platform.creationParams,
-          MatchesCreationParams(CreationParams(
-            initialUrl: 'https://youtube.com',
-            webSettings: WebSettings(
-              javascriptMode: JavascriptMode.disabled,
-              hasNavigationDelegate: false,
-              debuggingEnabled: false,
-              userAgent: const WebSetting<String?>.of(null),
-              gestureNavigationEnabled: true,
-              zoomEnabled: true,
-            ),
-          )));
-    });
-
-    testWidgets('loadUrl', (WidgetTester tester) async {
-      late WebViewController controller;
-      await tester.pumpWidget(
-        WebView(
-          initialUrl: 'https://youtube.com',
-          onWebViewCreated: (WebViewController webViewController) {
-            controller = webViewController;
-          },
-        ),
-      );
-
-      final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform;
-      final MyWebViewPlatformController platform = builder.lastPlatformBuilt!;
-
-      final Map<String, String> headers = <String, String>{
-        'header': 'value',
-      };
-
-      await controller.loadUrl('https://google.com', headers: headers);
-
-      expect(platform.lastUrlLoaded, 'https://google.com');
-      expect(platform.lastRequestHeaders, headers);
-    });
-  });
-
-  testWidgets('Set UserAgent', (WidgetTester tester) async {
-    await tester.pumpWidget(const WebView(
-      initialUrl: 'https://youtube.com',
-      javascriptMode: JavascriptMode.unrestricted,
-    ));
-
-    final CreationParams params = captureBuildArgs(
-      mockWebViewPlatform,
-      creationParams: true,
-    ).single as CreationParams;
-
-    expect(params.webSettings!.userAgent.value, isNull);
-
-    await tester.pumpWidget(const WebView(
-      initialUrl: 'https://youtube.com',
-      javascriptMode: JavascriptMode.unrestricted,
-      userAgent: 'UA',
-    ));
-
-    final WebSettings settings =
-        verify(mockWebViewPlatformController.updateSettings(captureAny))
-            .captured
-            .last as WebSettings;
-    expect(settings.userAgent.value, 'UA');
-  });
-}
-
-List<dynamic> captureBuildArgs(
-  MockWebViewPlatform mockWebViewPlatform, {
-  bool context = false,
-  bool creationParams = false,
-  bool webViewPlatformCallbacksHandler = false,
-  bool javascriptChannelRegistry = false,
-  bool onWebViewPlatformCreated = false,
-  bool gestureRecognizers = false,
-}) {
-  return verify(mockWebViewPlatform.build(
-    context: context ? captureAnyNamed('context') : anyNamed('context'),
-    creationParams: creationParams
-        ? captureAnyNamed('creationParams')
-        : anyNamed('creationParams'),
-    webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler
-        ? captureAnyNamed('webViewPlatformCallbacksHandler')
-        : anyNamed('webViewPlatformCallbacksHandler'),
-    javascriptChannelRegistry: javascriptChannelRegistry
-        ? captureAnyNamed('javascriptChannelRegistry')
-        : anyNamed('javascriptChannelRegistry'),
-    onWebViewPlatformCreated: onWebViewPlatformCreated
-        ? captureAnyNamed('onWebViewPlatformCreated')
-        : anyNamed('onWebViewPlatformCreated'),
-    gestureRecognizers: gestureRecognizers
-        ? captureAnyNamed('gestureRecognizers')
-        : anyNamed('gestureRecognizers'),
-  )).captured;
-}
-
-// This Widget ensures that onWebViewPlatformCreated is only called once when
-// making multiple calls to `WidgetTester.pumpWidget` with different parameters
-// for the WebView.
-class TestPlatformWebView extends StatefulWidget {
-  const TestPlatformWebView({
-    Key? key,
-    required this.mockWebViewPlatformController,
-    this.onWebViewPlatformCreated,
-  }) : super(key: key);
-
-  final MockWebViewPlatformController mockWebViewPlatformController;
-  final WebViewPlatformCreatedCallback? onWebViewPlatformCreated;
-
-  @override
-  State<StatefulWidget> createState() => TestPlatformWebViewState();
-}
-
-class TestPlatformWebViewState extends State<TestPlatformWebView> {
-  @override
-  void initState() {
-    super.initState();
-    final WebViewPlatformCreatedCallback? onWebViewPlatformCreated =
-        widget.onWebViewPlatformCreated;
-    if (onWebViewPlatformCreated != null) {
-      onWebViewPlatformCreated(widget.mockWebViewPlatformController);
-    }
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Container();
-  }
-}
-
-class MyWebViewPlatform implements WebViewPlatform {
-  MyWebViewPlatformController? lastPlatformBuilt;
-
-  @override
-  Widget build({
-    BuildContext? context,
-    CreationParams? creationParams,
-    required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
-    required JavascriptChannelRegistry javascriptChannelRegistry,
-    WebViewPlatformCreatedCallback? onWebViewPlatformCreated,
-    Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
-  }) {
-    assert(onWebViewPlatformCreated != null);
-    lastPlatformBuilt = MyWebViewPlatformController(
-        creationParams, gestureRecognizers, webViewPlatformCallbacksHandler);
-    onWebViewPlatformCreated!(lastPlatformBuilt);
-    return Container();
-  }
-
-  @override
-  Future<bool> clearCookies() {
-    return Future<bool>.sync(() => true);
-  }
-}
-
-class MyWebViewPlatformController extends WebViewPlatformController {
-  MyWebViewPlatformController(this.creationParams, this.gestureRecognizers,
-      WebViewPlatformCallbacksHandler platformHandler)
-      : super(platformHandler);
-
-  CreationParams? creationParams;
-  Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers;
-
-  String? lastUrlLoaded;
-  Map<String, String>? lastRequestHeaders;
-
-  @override
-  Future<void> loadUrl(String url, Map<String, String>? headers) async {
-    equals(1, 1);
-    lastUrlLoaded = url;
-    lastRequestHeaders = headers;
-  }
-}
-
-class MatchesWebSettings extends Matcher {
-  MatchesWebSettings(this._webSettings);
-
-  final WebSettings? _webSettings;
-
-  @override
-  Description describe(Description description) =>
-      description.add('$_webSettings');
-
-  @override
-  bool matches(
-      covariant WebSettings webSettings, Map<dynamic, dynamic> matchState) {
-    return _webSettings!.javascriptMode == webSettings.javascriptMode &&
-        _webSettings!.hasNavigationDelegate ==
-            webSettings.hasNavigationDelegate &&
-        _webSettings!.debuggingEnabled == webSettings.debuggingEnabled &&
-        _webSettings!.gestureNavigationEnabled ==
-            webSettings.gestureNavigationEnabled &&
-        _webSettings!.userAgent == webSettings.userAgent &&
-        _webSettings!.zoomEnabled == webSettings.zoomEnabled;
-  }
-}
-
-class MatchesCreationParams extends Matcher {
-  MatchesCreationParams(this._creationParams);
-
-  final CreationParams _creationParams;
-
-  @override
-  Description describe(Description description) =>
-      description.add('$_creationParams');
-
-  @override
-  bool matches(covariant CreationParams creationParams,
-      Map<dynamic, dynamic> matchState) {
-    return _creationParams.initialUrl == creationParams.initialUrl &&
-        MatchesWebSettings(_creationParams.webSettings)
-            .matches(creationParams.webSettings!, matchState) &&
-        orderedEquals(_creationParams.javascriptChannelNames)
-            .matches(creationParams.javascriptChannelNames, matchState);
-  }
-}
-
-class MockWebViewCookieManagerPlatform extends WebViewCookieManagerPlatform {
-  List<WebViewCookie> setCookieCalls = <WebViewCookie>[];
-
-  @override
-  Future<bool> clearCookies() async => true;
-
-  @override
-  Future<void> setCookie(WebViewCookie cookie) async {
-    setCookieCalls.add(cookie);
-  }
-
-  void reset() {
-    setCookieCalls = <WebViewCookie>[];
-  }
 }
diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart
deleted file mode 100644
index a7a2100..0000000
--- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart
+++ /dev/null
@@ -1,213 +0,0 @@
-// Mocks generated by Mockito 5.3.0 from annotations
-// in webview_flutter/test/webview_flutter_test.dart.
-// Do not manually edit this file.
-
-// ignore_for_file: no_leading_underscores_for_library_prefixes
-import 'dart:async' as _i9;
-
-import 'package:flutter/foundation.dart' as _i3;
-import 'package:flutter/gestures.dart' as _i8;
-import 'package:flutter/widgets.dart' as _i2;
-import 'package:mockito/mockito.dart' as _i1;
-import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart'
-    as _i7;
-import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform.dart'
-    as _i4;
-import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_callbacks_handler.dart'
-    as _i6;
-import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_controller.dart'
-    as _i10;
-import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i5;
-
-// 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
-// ignore_for_file: subtype_of_sealed_class
-
-class _FakeWidget_0 extends _i1.SmartFake implements _i2.Widget {
-  _FakeWidget_0(Object parent, Invocation parentInvocation)
-      : super(parent, parentInvocation);
-
-  @override
-  String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) =>
-      super.toString();
-}
-
-/// A class which mocks [WebViewPlatform].
-///
-/// See the documentation for Mockito's code generation for more information.
-class MockWebViewPlatform extends _i1.Mock implements _i4.WebViewPlatform {
-  MockWebViewPlatform() {
-    _i1.throwOnMissingStub(this);
-  }
-
-  @override
-  _i2.Widget build(
-          {_i2.BuildContext? context,
-          _i5.CreationParams? creationParams,
-          _i6.WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler,
-          _i7.JavascriptChannelRegistry? javascriptChannelRegistry,
-          _i4.WebViewPlatformCreatedCallback? onWebViewPlatformCreated,
-          Set<_i3.Factory<_i8.OneSequenceGestureRecognizer>>?
-              gestureRecognizers}) =>
-      (super.noSuchMethod(
-          Invocation.method(#build, [], {
-            #context: context,
-            #creationParams: creationParams,
-            #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler,
-            #javascriptChannelRegistry: javascriptChannelRegistry,
-            #onWebViewPlatformCreated: onWebViewPlatformCreated,
-            #gestureRecognizers: gestureRecognizers
-          }),
-          returnValue: _FakeWidget_0(
-              this,
-              Invocation.method(#build, [], {
-                #context: context,
-                #creationParams: creationParams,
-                #webViewPlatformCallbacksHandler:
-                    webViewPlatformCallbacksHandler,
-                #javascriptChannelRegistry: javascriptChannelRegistry,
-                #onWebViewPlatformCreated: onWebViewPlatformCreated,
-                #gestureRecognizers: gestureRecognizers
-              }))) as _i2.Widget);
-  @override
-  _i9.Future<bool> clearCookies() =>
-      (super.noSuchMethod(Invocation.method(#clearCookies, []),
-          returnValue: _i9.Future<bool>.value(false)) as _i9.Future<bool>);
-}
-
-/// A class which mocks [WebViewPlatformController].
-///
-/// See the documentation for Mockito's code generation for more information.
-class MockWebViewPlatformController extends _i1.Mock
-    implements _i10.WebViewPlatformController {
-  MockWebViewPlatformController() {
-    _i1.throwOnMissingStub(this);
-  }
-
-  @override
-  _i9.Future<void> loadFile(String? absoluteFilePath) => (super.noSuchMethod(
-      Invocation.method(#loadFile, [absoluteFilePath]),
-      returnValue: _i9.Future<void>.value(),
-      returnValueForMissingStub: _i9.Future<void>.value()) as _i9.Future<void>);
-  @override
-  _i9.Future<void> loadFlutterAsset(String? key) => (super.noSuchMethod(
-      Invocation.method(#loadFlutterAsset, [key]),
-      returnValue: _i9.Future<void>.value(),
-      returnValueForMissingStub: _i9.Future<void>.value()) as _i9.Future<void>);
-  @override
-  _i9.Future<void> loadHtmlString(String? html, {String? baseUrl}) =>
-      (super.noSuchMethod(
-              Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}),
-              returnValue: _i9.Future<void>.value(),
-              returnValueForMissingStub: _i9.Future<void>.value())
-          as _i9.Future<void>);
-  @override
-  _i9.Future<void> loadUrl(String? url, Map<String, String>? headers) =>
-      (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]),
-              returnValue: _i9.Future<void>.value(),
-              returnValueForMissingStub: _i9.Future<void>.value())
-          as _i9.Future<void>);
-  @override
-  _i9.Future<void> loadRequest(_i5.WebViewRequest? request) =>
-      (super.noSuchMethod(Invocation.method(#loadRequest, [request]),
-              returnValue: _i9.Future<void>.value(),
-              returnValueForMissingStub: _i9.Future<void>.value())
-          as _i9.Future<void>);
-  @override
-  _i9.Future<void> updateSettings(_i5.WebSettings? setting) =>
-      (super.noSuchMethod(Invocation.method(#updateSettings, [setting]),
-              returnValue: _i9.Future<void>.value(),
-              returnValueForMissingStub: _i9.Future<void>.value())
-          as _i9.Future<void>);
-  @override
-  _i9.Future<String?> currentUrl() =>
-      (super.noSuchMethod(Invocation.method(#currentUrl, []),
-          returnValue: _i9.Future<String?>.value()) as _i9.Future<String?>);
-  @override
-  _i9.Future<bool> canGoBack() =>
-      (super.noSuchMethod(Invocation.method(#canGoBack, []),
-          returnValue: _i9.Future<bool>.value(false)) as _i9.Future<bool>);
-  @override
-  _i9.Future<bool> canGoForward() =>
-      (super.noSuchMethod(Invocation.method(#canGoForward, []),
-          returnValue: _i9.Future<bool>.value(false)) as _i9.Future<bool>);
-  @override
-  _i9.Future<void> goBack() => (super.noSuchMethod(
-      Invocation.method(#goBack, []),
-      returnValue: _i9.Future<void>.value(),
-      returnValueForMissingStub: _i9.Future<void>.value()) as _i9.Future<void>);
-  @override
-  _i9.Future<void> goForward() => (super.noSuchMethod(
-      Invocation.method(#goForward, []),
-      returnValue: _i9.Future<void>.value(),
-      returnValueForMissingStub: _i9.Future<void>.value()) as _i9.Future<void>);
-  @override
-  _i9.Future<void> reload() => (super.noSuchMethod(
-      Invocation.method(#reload, []),
-      returnValue: _i9.Future<void>.value(),
-      returnValueForMissingStub: _i9.Future<void>.value()) as _i9.Future<void>);
-  @override
-  _i9.Future<void> clearCache() => (super.noSuchMethod(
-      Invocation.method(#clearCache, []),
-      returnValue: _i9.Future<void>.value(),
-      returnValueForMissingStub: _i9.Future<void>.value()) as _i9.Future<void>);
-  @override
-  _i9.Future<String> evaluateJavascript(String? javascript) =>
-      (super.noSuchMethod(Invocation.method(#evaluateJavascript, [javascript]),
-          returnValue: _i9.Future<String>.value('')) as _i9.Future<String>);
-  @override
-  _i9.Future<void> runJavascript(String? javascript) => (super.noSuchMethod(
-      Invocation.method(#runJavascript, [javascript]),
-      returnValue: _i9.Future<void>.value(),
-      returnValueForMissingStub: _i9.Future<void>.value()) as _i9.Future<void>);
-  @override
-  _i9.Future<String> runJavascriptReturningResult(String? javascript) =>
-      (super.noSuchMethod(
-          Invocation.method(#runJavascriptReturningResult, [javascript]),
-          returnValue: _i9.Future<String>.value('')) as _i9.Future<String>);
-  @override
-  _i9.Future<void> addJavascriptChannels(Set<String>? javascriptChannelNames) =>
-      (super.noSuchMethod(
-          Invocation.method(#addJavascriptChannels, [javascriptChannelNames]),
-          returnValue: _i9.Future<void>.value(),
-          returnValueForMissingStub:
-              _i9.Future<void>.value()) as _i9.Future<void>);
-  @override
-  _i9.Future<void> removeJavascriptChannels(
-          Set<String>? javascriptChannelNames) =>
-      (super.noSuchMethod(
-              Invocation.method(
-                  #removeJavascriptChannels, [javascriptChannelNames]),
-              returnValue: _i9.Future<void>.value(),
-              returnValueForMissingStub: _i9.Future<void>.value())
-          as _i9.Future<void>);
-  @override
-  _i9.Future<String?> getTitle() =>
-      (super.noSuchMethod(Invocation.method(#getTitle, []),
-          returnValue: _i9.Future<String?>.value()) as _i9.Future<String?>);
-  @override
-  _i9.Future<void> scrollTo(int? x, int? y) => (super.noSuchMethod(
-      Invocation.method(#scrollTo, [x, y]),
-      returnValue: _i9.Future<void>.value(),
-      returnValueForMissingStub: _i9.Future<void>.value()) as _i9.Future<void>);
-  @override
-  _i9.Future<void> scrollBy(int? x, int? y) => (super.noSuchMethod(
-      Invocation.method(#scrollBy, [x, y]),
-      returnValue: _i9.Future<void>.value(),
-      returnValueForMissingStub: _i9.Future<void>.value()) as _i9.Future<void>);
-  @override
-  _i9.Future<int> getScrollX() =>
-      (super.noSuchMethod(Invocation.method(#getScrollX, []),
-          returnValue: _i9.Future<int>.value(0)) as _i9.Future<int>);
-  @override
-  _i9.Future<int> getScrollY() =>
-      (super.noSuchMethod(Invocation.method(#getScrollY, []),
-          returnValue: _i9.Future<int>.value(0)) as _i9.Future<int>);
-}
diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.dart b/packages/webview_flutter/webview_flutter/test/webview_widget_test.dart
similarity index 90%
rename from packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.dart
rename to packages/webview_flutter/webview_flutter/test/webview_widget_test.dart
index 455d8b3..21e9f53 100644
--- a/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.dart
+++ b/packages/webview_flutter/webview_flutter/test/webview_widget_test.dart
@@ -8,8 +8,8 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
-import 'package:webview_flutter/src/v4/webview_flutter.dart';
-import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart';
+import 'package:webview_flutter/webview_flutter.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
 
 import 'webview_widget_test.mocks.dart';
 
@@ -77,8 +77,7 @@
 }
 
 class TestPlatformWebViewWidget extends PlatformWebViewWidget {
-  TestPlatformWebViewWidget(PlatformWebViewWidgetCreationParams params)
-      : super.implementation(params);
+  TestPlatformWebViewWidget(super.params) : super.implementation();
 
   @override
   Widget build(BuildContext context) {
diff --git a/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart
new file mode 100644
index 0000000..0e29ede
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart
@@ -0,0 +1,396 @@
+// Mocks generated by Mockito 5.3.2 from annotations
+// in webview_flutter/test/webview_widget_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i7;
+import 'dart:ui' as _i3;
+
+import 'package:flutter/foundation.dart' as _i5;
+import 'package:flutter/widgets.dart' as _i4;
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart'
+    as _i8;
+import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart'
+    as _i6;
+import 'package:webview_flutter_platform_interface/src/platform_webview_widget.dart'
+    as _i9;
+import 'package:webview_flutter_platform_interface/src/webview_platform.dart'
+    as _i2;
+
+// 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
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakePlatformWebViewControllerCreationParams_0 extends _i1.SmartFake
+    implements _i2.PlatformWebViewControllerCreationParams {
+  _FakePlatformWebViewControllerCreationParams_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakeObject_1 extends _i1.SmartFake implements Object {
+  _FakeObject_1(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakeOffset_2 extends _i1.SmartFake implements _i3.Offset {
+  _FakeOffset_2(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakePlatformWebViewWidgetCreationParams_3 extends _i1.SmartFake
+    implements _i2.PlatformWebViewWidgetCreationParams {
+  _FakePlatformWebViewWidgetCreationParams_3(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakeWidget_4 extends _i1.SmartFake implements _i4.Widget {
+  _FakeWidget_4(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+
+  @override
+  String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) =>
+      super.toString();
+}
+
+/// A class which mocks [PlatformWebViewController].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockPlatformWebViewController extends _i1.Mock
+    implements _i6.PlatformWebViewController {
+  MockPlatformWebViewController() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i2.PlatformWebViewControllerCreationParams get params => (super.noSuchMethod(
+        Invocation.getter(#params),
+        returnValue: _FakePlatformWebViewControllerCreationParams_0(
+          this,
+          Invocation.getter(#params),
+        ),
+      ) as _i2.PlatformWebViewControllerCreationParams);
+  @override
+  _i7.Future<void> loadFile(String? absoluteFilePath) => (super.noSuchMethod(
+        Invocation.method(
+          #loadFile,
+          [absoluteFilePath],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> loadFlutterAsset(String? key) => (super.noSuchMethod(
+        Invocation.method(
+          #loadFlutterAsset,
+          [key],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> loadHtmlString(
+    String? html, {
+    String? baseUrl,
+  }) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #loadHtmlString,
+          [html],
+          {#baseUrl: baseUrl},
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> loadRequest(_i2.LoadRequestParams? params) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #loadRequest,
+          [params],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<String?> currentUrl() => (super.noSuchMethod(
+        Invocation.method(
+          #currentUrl,
+          [],
+        ),
+        returnValue: _i7.Future<String?>.value(),
+      ) as _i7.Future<String?>);
+  @override
+  _i7.Future<bool> canGoBack() => (super.noSuchMethod(
+        Invocation.method(
+          #canGoBack,
+          [],
+        ),
+        returnValue: _i7.Future<bool>.value(false),
+      ) as _i7.Future<bool>);
+  @override
+  _i7.Future<bool> canGoForward() => (super.noSuchMethod(
+        Invocation.method(
+          #canGoForward,
+          [],
+        ),
+        returnValue: _i7.Future<bool>.value(false),
+      ) as _i7.Future<bool>);
+  @override
+  _i7.Future<void> goBack() => (super.noSuchMethod(
+        Invocation.method(
+          #goBack,
+          [],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> goForward() => (super.noSuchMethod(
+        Invocation.method(
+          #goForward,
+          [],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> reload() => (super.noSuchMethod(
+        Invocation.method(
+          #reload,
+          [],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> clearCache() => (super.noSuchMethod(
+        Invocation.method(
+          #clearCache,
+          [],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> clearLocalStorage() => (super.noSuchMethod(
+        Invocation.method(
+          #clearLocalStorage,
+          [],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> setPlatformNavigationDelegate(
+          _i8.PlatformNavigationDelegate? handler) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setPlatformNavigationDelegate,
+          [handler],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> runJavaScript(String? javaScript) => (super.noSuchMethod(
+        Invocation.method(
+          #runJavaScript,
+          [javaScript],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<Object> runJavaScriptReturningResult(String? javaScript) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #runJavaScriptReturningResult,
+          [javaScript],
+        ),
+        returnValue: _i7.Future<Object>.value(_FakeObject_1(
+          this,
+          Invocation.method(
+            #runJavaScriptReturningResult,
+            [javaScript],
+          ),
+        )),
+      ) as _i7.Future<Object>);
+  @override
+  _i7.Future<void> addJavaScriptChannel(
+          _i6.JavaScriptChannelParams? javaScriptChannelParams) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #addJavaScriptChannel,
+          [javaScriptChannelParams],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> removeJavaScriptChannel(String? javaScriptChannelName) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #removeJavaScriptChannel,
+          [javaScriptChannelName],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<String?> getTitle() => (super.noSuchMethod(
+        Invocation.method(
+          #getTitle,
+          [],
+        ),
+        returnValue: _i7.Future<String?>.value(),
+      ) as _i7.Future<String?>);
+  @override
+  _i7.Future<void> scrollTo(
+    int? x,
+    int? y,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #scrollTo,
+          [
+            x,
+            y,
+          ],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> scrollBy(
+    int? x,
+    int? y,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #scrollBy,
+          [
+            x,
+            y,
+          ],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<_i3.Offset> getScrollPosition() => (super.noSuchMethod(
+        Invocation.method(
+          #getScrollPosition,
+          [],
+        ),
+        returnValue: _i7.Future<_i3.Offset>.value(_FakeOffset_2(
+          this,
+          Invocation.method(
+            #getScrollPosition,
+            [],
+          ),
+        )),
+      ) as _i7.Future<_i3.Offset>);
+  @override
+  _i7.Future<void> enableZoom(bool? enabled) => (super.noSuchMethod(
+        Invocation.method(
+          #enableZoom,
+          [enabled],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> setBackgroundColor(_i3.Color? color) => (super.noSuchMethod(
+        Invocation.method(
+          #setBackgroundColor,
+          [color],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> setJavaScriptMode(_i2.JavaScriptMode? javaScriptMode) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #setJavaScriptMode,
+          [javaScriptMode],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+  @override
+  _i7.Future<void> setUserAgent(String? userAgent) => (super.noSuchMethod(
+        Invocation.method(
+          #setUserAgent,
+          [userAgent],
+        ),
+        returnValue: _i7.Future<void>.value(),
+        returnValueForMissingStub: _i7.Future<void>.value(),
+      ) as _i7.Future<void>);
+}
+
+/// A class which mocks [PlatformWebViewWidget].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockPlatformWebViewWidget extends _i1.Mock
+    implements _i9.PlatformWebViewWidget {
+  MockPlatformWebViewWidget() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i2.PlatformWebViewWidgetCreationParams get params => (super.noSuchMethod(
+        Invocation.getter(#params),
+        returnValue: _FakePlatformWebViewWidgetCreationParams_3(
+          this,
+          Invocation.getter(#params),
+        ),
+      ) as _i2.PlatformWebViewWidgetCreationParams);
+  @override
+  _i4.Widget build(_i4.BuildContext? context) => (super.noSuchMethod(
+        Invocation.method(
+          #build,
+          [context],
+        ),
+        returnValue: _FakeWidget_4(
+          this,
+          Invocation.method(
+            #build,
+            [context],
+          ),
+        ),
+      ) as _i4.Widget);
+}
diff --git a/script/configs/exclude_all_plugins_app.yaml b/script/configs/exclude_all_plugins_app.yaml
index a19f5fe..8dd0fde 100644
--- a/script/configs/exclude_all_plugins_app.yaml
+++ b/script/configs/exclude_all_plugins_app.yaml
@@ -8,10 +8,3 @@
 
 # This is a permament entry, as it should never be a direct app dependency.
 - plugin_platform_interface
-# Packages below are temporarily added to push and release a new webview
-# interface. Remove packages with release of `webview_flutter` 4.0.0. See
-# https://github.com/flutter/flutter/issues/94051.
-- webview_flutter_platform_interface
-- webview_flutter_wkwebview
-- webview_flutter_android
-- webview_flutter_web
diff --git a/script/configs/temp_exclude_excerpt.yaml b/script/configs/temp_exclude_excerpt.yaml
index c59983e..4034629 100644
--- a/script/configs/temp_exclude_excerpt.yaml
+++ b/script/configs/temp_exclude_excerpt.yaml
@@ -18,6 +18,5 @@
 - plugin_platform_interface
 - quick_actions/quick_actions
 - shared_preferences/shared_preferences
-- webview_flutter/webview_flutter
 - webview_flutter_android
 - webview_flutter_web