Add a platform view test to android_hardware_smoke_test (#187913)

Expand test coverage to address more of
https://github.com/flutter/flutter/issues/182123

This adds a test with a platform view. In order to get both the flutter
pixels and the native pixels fully composited, we have to initiate the
screenshot from the test runner, not the app. This has a few
consequences for the architecture and data flow of the tests. Details in
the changes to README.md.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/dev/integration_tests/android_hardware_smoke_test/README.md b/dev/integration_tests/android_hardware_smoke_test/README.md
index 0257f58..06598d7 100644
--- a/dev/integration_tests/android_hardware_smoke_test/README.md
+++ b/dev/integration_tests/android_hardware_smoke_test/README.md
@@ -36,42 +36,34 @@
 
 ## 3. Dual-Mode Architecture
 
+```mermaid
+flowchart TD
+    TestInit(["Test Initiation"]) --> Mode{"Which Mode?"}
+
+    Mode -->|"Host-Driven Mode"| HostScript["driver script<br/>(test_driver/driver_test.dart)"]
+    Mode -->|"Instrumented Mode"| JUnit["native Android JUnit<br/>(FlutterActivityTest)"]
+
+    HostScript --> RequestData["Driver script connects and requests<br/>testName via driver.requestData()"]
+    JUnit --> SendMessage["JUnit test runner sends<br/>testName over Message Channel"]
+
+    RequestData --> RenderHost["Dart app renders target state"]
+    SendMessage --> RenderDevice["Dart app renders target state"]
+
+    RenderHost --> HostRender["Dart app returns image bytes over channel"]
+    RenderDevice --> OnDeviceCompare["Dart app performs local on-device golden<br/>comparison against bundled assets"]
+
+    HostRender --> CompareHost["Driver script asserts exact match<br/>against local filesystem"]
+    OnDeviceCompare --> Report["Dart app returns success/fail<br/>to JUnit test runner"]
 ```
-                        ┌──────────────────────────────────────┐
-                        │           Test Initiation            │
-                        └──────────────────┬───────────────────┘
-                                           │
-                  ┌────────────────────────┴────────────────────────┐
-                  ▼                                                 ▼
-       [ On-Device Instrumented ]                          [ Host-Driven Driver ]
-    (OEM / Self-Contained Testing)                       (CI / Host-Driven Testing)
-                  │                                                 │
-                  ▼                                                 ▼
-        native Android JUnit                              flutter drive host script
-  (FlutterActivityTest / AndroidJUnit4)             (test_driver/driver_test.dart)
-                  │                                                 │
-                  ▼                                                 ▼
-        Native Java Test sends                            Host driver script connects,
-     testName over Message Channel                       and requests testName via
-                  │                                      driver.requestData()
-                  ▼                                                 │
-     App renders target state &                                     ▼
-   performs local on-device golden                       App renders target state and
-   comparison against bundled assets                     returns image bytes over channel
-                  │                                                 │
-                  ▼                                                 ▼
-     App replies status back to                          Host driver script decodes
-      Java; JUnit reports result                         image bytes and asserts match
-```
+
+### Host-Driven Driver Mode (CI / Host-Driven)
+* **Orchestration**: Orchestrated by the host PC using `flutter drive`.
+* **Execution**: The host script (`test_driver/driver_test.dart`) commands the app (`lib/main.dart` through thin wrapper `integration_test/integration_test_wrapper.dart`) to transition states. The app captures the repaint boundary, base64-encodes it, and streams the bytes back to the host the message channel. The host driver decodes the bytes and asserts visual matches against local repository baselines on the host filesystem.
 
 ### Instrumented On-Device Mode (OEM / Standalone)
 * **Orchestration**: Runs purely on the device under Android `AndroidJUnit4` runner.
 * **Execution**: Java JUnit code (`FlutterActivityTest.java`) launches the main activity and sends the test payload over a JSON message channel. The app (`lib/main.dart`) renders the widget, performs a local pixel-by-pixel on-device comparison against baseline images bundled within the APK assets, and replies with the status to the Java runner to pass or fail the JUnit assertion.
 
-### Host-Driven Driver Mode (CI / Host-Driven)
-* **Orchestration**: Orchestrated by the host PC using `flutter drive`.
-* **Execution**: The host script (`test_driver/driver_test.dart`) commands the target app (`integration_test/integration_test_wrapper.dart`) to transition states. The app captures the repaint boundary, base64-encodes it, and streams the bytes back to the host over a WebSocket channel. The host driver decodes the bytes and asserts visual matches against local repository baselines on the host filesystem.
-
 ---
 
 ## 4. How to Run the Tests
@@ -204,4 +196,75 @@
 | **`textTest`** | **Font Rendering** | Text layout (`TextPainter`), glyph caching, shaping, and font atlas rendering. | Simple `TextPainter.paint` |
 | **`imageTest`** | **Texture Sampling** | Image decoding, GPU texture uploading, and texture sampler rendering. Uses a 32x32 4-color checkerboard PNG to verify RGB color channel correctness. | Simple `canvas.drawImage` |
 | **`advancedBlendTest`** | **Advanced Blending** | Fragment shader blending and framebuffer fetch tile-memory optimizations (e.g. Vulkan subpass inputs, `EXT_shader_framebuffer_fetch` in GLES). Uses `BlendMode.difference`. | Mirrors [animated_advanced_blend.dart](/dev/benchmarks/macrobenchmarks/lib/src/animated_advanced_blend.dart). |
-| **`backdropFilterBlurTest`** | **Compositing & Blur** | Offscreen texture allocation, layer downscale/upscale passes, and multi-pass Gaussian blur filter execution. Uses `ImageFilter.blur(sigmaX: 5, sigmaY: 5)`. | Mirrors [backdrop_filter.dart](/dev/benchmarks/macrobenchmarks/lib/src/backdrop_filter.dart). |
\ No newline at end of file
+| **`backdropFilterBlurTest`** | **Compositing & Blur** | Offscreen texture allocation, layer downscale/upscale passes, and multi-pass Gaussian blur filter execution. Uses `ImageFilter.blur(sigmaX: 5, sigmaY: 5)`. | Mirrors [backdrop_filter.dart](/dev/benchmarks/macrobenchmarks/lib/src/backdrop_filter.dart). |
+| **`platformViewTest`** | **Platform Views** | Embedded native Android views composition, texture layer allocation, platform/raster thread synchronization, and system compositor screenshot capture. Uses `AndroidViewSurface` under Hybrid Composition. | Mirrors [hybrid_android_views](/dev/integration_tests/hybrid_android_views) and [android_views](/dev/integration_tests/android_views) layout structures. |
+
+---
+
+## 6. Platform View Screenshot Strategy
+
+Testing platform views requires capturing a screenshot of the physical screen layout that contains both the Flutter-rendered UI and native Android views (e.g., a native `TextView` wrapped under hybrid composition).
+
+Because these two contexts are rendered on separate hardware surface layers, standard in-process widget screenshot methods (like `RenderRepaintBoundary.toImage()`) cannot see or capture the native platform view pixels.
+
+To address this, the test suite implements a **No-Compositing System Screenshot Strategy**:
+
+```mermaid
+flowchart TD
+    TestInit(["Test Initiation"]) --> Mode{"Which Mode?"}
+
+    Mode -->|"Host-Driven Mode"| HostScript["driver script<br/>(test_driver/driver_test.dart)"]
+    Mode -->|"Instrumented Mode"| JUnit["native Android JUnit<br/>(FlutterActivityTest)"]
+
+    HostScript --> RequestData["Driver script connects and requests<br/>testName via driver.requestData()"]
+    JUnit --> SendMessage["JUnit test runner sends<br/>testName over Message Channel"]
+
+    RequestData --> RenderHost["Dart app renders target state"]
+    SendMessage --> RenderDevice["Dart app renders target state"]
+
+    RenderHost -->|"Unique for PlatformView tests"| RenderHostPlatformView["Native Kotlin UI renders via PlatformView"]:::unique
+    RenderDevice -->|"Unique for PlatformView tests"| RenderDevicePlatformView["Native Kotlin UI renders via PlatformView"]:::unique
+
+    RenderHostPlatformView --> CoordsHost["Dart app returns widget<br/>crop coordinates"]:::unique
+    RenderDevicePlatformView --> CoordsDevice["Dart app returns widget<br/>crop coordinates"]:::unique
+
+    CoordsHost --> CaptureHost["Driver script takes ADB screencap<br/>and crops locally"]:::unique
+    CoordsDevice --> CaptureDevice["JUnit test runner takes UiAutomation<br/>screencap and crops natively"]:::unique
+
+    CaptureHost -->|"Remaining steps occur normally"| CompareHost["Driver script asserts exact match<br/>against local filesystem"]
+    CaptureDevice --> SendBytes["JUnit test runner sends cropped<br/>base64 bytes to Dart app over message channel"]:::unique
+
+    SendBytes --> ImmediateComparison["App handles cropped bytes immediately without new frame render"]:::unique
+
+    ImmediateComparison -->|"Remaining steps occur normally"| OnDeviceCompare["Dart app performs local on-device golden<br/>comparison against bundled assets"]
+
+    OnDeviceCompare --> Report["Dart app returns success/fail<br/>to JUnit test runner"]
+
+    classDef unique fill:#fff3cd,stroke:#ffc107,color:#856404,stroke-width:2px;
+```
+
+### Flow Breakdown
+
+The beginning and end of the test are the same as for other tests as described in Section 3 above.
+
+* **Native UI rendering and timing**: For `platformViewTest`, the app renders an `AndroidView` widget. This causes the Android OS to render the native Kotlin UI (`NativeTextView`) via Hybrid Composition ([`NativeTextView.kt`](android/app/src/main/kotlin/com/example/android_hardware_smoke_test/NativeTextView.kt)). Because the native UI is rendered by the OS, `addPostFrameCallback` is no longer sufficient to guarantee that all pixels of the test scenario have been fully rendered and composited, so `handleGoldenRequest` waits a few extra frames using `await WidgetsBinding.instance.endOfFrame`.
+* **Crop coordinates and test-script-driven screenshot capture**:
+  * **Both Modes:** The Dart app ([`goldens.dart: platformViewTest`](lib/goldens.dart)) computes the bounding box of the `RepaintBoundary` in physical device pixels and returns these coordinates back to the test runner.
+  * **Host-Driven Mode:** The driver script takes a full-screen screenshot of the physical device using ADB (`adb shell screencap` wrapped inside `NativeDriver.screenshot()` in [`driver_test.dart`](test_driver/driver_test.dart)). Then crops it locally to the retrieved coordinates using the Dart `image` package.
+  * **Instrumented Mode:** The JUnit test runner takes a full-screen screenshot using `UiAutomation.takeScreenshot()` in [`FlutterActivityTest.kt: captureAndSendScreenshot`](android/app/src/androidTest/java/com/example/android_hardware_smoke_test/FlutterActivityTest.kt). Then crops the bitmap natively using `Bitmap.createBitmap`.
+* **Extra round trip and encoding-independent pixel comparison for Instrumented Mode**:
+  * **Instrumented Mode:** The Dart app is where we perform the golden comparison, so the JUnit test runner sends the cropped bytes back to the Dart app by base64-encoding them and sending them over the `BasicMessageChannel`. It sets a field on the JSON message called `command` with the value `compare_golden`. When the Dart app parses this request, it performs the golden comparison immediately instead of waiting for another frame through `addPostFrameCallback`. However, the normal `NaiveLocalFileComparator` compares all image bytes. This is a problem because the different methods of capturing screenshots may produce images with PNG encoding differences. To compare pixels regardless of encoding, we use a [PixelExactLocalFileComparator](lib/pixel_exact_local_file_comparator.dart). After comparison, it reports success or failure in the same way as it would for other tests.
+  * **Host-Driven Mode:** The driver doesn't need a new round trip because it performs the comparison directly in the same way as it does for image bytes returned from the Dart app.
+
+
+### Why the `PixelCopy` approach was not desirable
+
+An alternative approach using `PixelCopy` was considered:
+* **How it worked:** The native Kotlin app targeted the `FlutterSurfaceView` directly using `PixelCopy.request(surfaceView, ...)` and then manually traversed the Android sibling view hierarchy, drawing the visible native platform view boundaries on top of the captured bitmap using a Kotlin `Canvas`.
+
+While plausible and functioning, it was **not desirable** for several reasons:
+1. **Manual Compositing Replicas:** Drawing views manually onto a canvas (`child.draw(canvas)`) relies on replicating the composition steps. If the operating system or graphics drivers apply specific shader effects, blending, custom overlays, or subpixel anti-aliasing during hardware composition, the manual Kotlin reconstruction might not match what the user actually sees.
+2. **Missing Real Composition Bugs:** The primary goal of this smoke test is to catch platform-specific integration and composition rendering errors in GPU drivers. If we manually draw the sibling views ourselves, we bypass the OS hardware compositor (SurfaceFlinger) entirely for the screenshot, defeating the purpose of testing the system's actual composition pipeline.
+3. **Fragility:** Manually translating coordinate spaces, handling layouts, view visibility states, and Z-orders in Kotlin is highly fragile and prone to emulation/rendering bugs.
+
+By capturing the entire screen using native system compositor APIs (`adb` / `UiAutomation`) and cropping, the test asserts on the **true, final composited image** rendered by the device's GPU and system composer.
diff --git a/dev/integration_tests/android_hardware_smoke_test/android/app/src/androidTest/java/com/example/android_hardware_smoke_test/FlutterActivityTest.kt b/dev/integration_tests/android_hardware_smoke_test/android/app/src/androidTest/java/com/example/android_hardware_smoke_test/FlutterActivityTest.kt
index 51ce693..7fe1ecc 100644
--- a/dev/integration_tests/android_hardware_smoke_test/android/app/src/androidTest/java/com/example/android_hardware_smoke_test/FlutterActivityTest.kt
+++ b/dev/integration_tests/android_hardware_smoke_test/android/app/src/androidTest/java/com/example/android_hardware_smoke_test/FlutterActivityTest.kt
@@ -2,17 +2,23 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+@file:Suppress("PackageName")
+
 package com.example.android_hardware_smoke_test
 
+import android.graphics.Bitmap
+import android.util.Base64
 import android.util.Log
 import androidx.lifecycle.Lifecycle
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
 import org.json.JSONObject
 import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import java.io.ByteArrayOutputStream
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
@@ -21,6 +27,10 @@
 class FlutterActivityTest {
     companion object {
         private const val TAG = "FlutterActivityTest"
+        private const val PLATFORM_VIEW_TEST_NAME = "platformViewTest"
+        private const val SCREENSHOT_CAPTURE_DELAY_MS = 200L
+        private const val DIAGNOSTIC_WARNING_DELAY_SEC = 5L
+        private const val TEST_TIMEOUT_SEC = 60L
     }
 
     @get:Rule val rule = ActivityScenarioRule(MainActivity::class.java)
@@ -44,18 +54,32 @@
             assertEquals(Lifecycle.State.RESUMED, activity.lifecycle.currentState)
 
             try {
+                val isPlatformView = testName == PLATFORM_VIEW_TEST_NAME
                 val message =
                     JSONObject().apply {
                         put("testName", testName)
-                        put("performAppSideGoldenCompare", true)
+                        put("performAppSideGoldenCompare", !isPlatformView)
                     }
 
                 Log.d(TAG, "Sending '$message' on message channel")
 
                 activity.messageChannel?.send(message) { reply ->
                     try {
-                        val replyJson = reply as JSONObject
-                        future.complete(replyJson.getString("message"))
+                        val replyJson =
+                            reply as? JSONObject
+                                ?: throw IllegalStateException("Expected JSONObject reply from Dart, but received: $reply")
+                        val replyMessage = replyJson.getString("message")
+
+                        if (isPlatformView && replyMessage == "Rendered $PLATFORM_VIEW_TEST_NAME") {
+                            val x = replyJson.getInt("x")
+                            val y = replyJson.getInt("y")
+                            val width = replyJson.getInt("width")
+                            val height = replyJson.getInt("height")
+
+                            captureAndSendScreenshot(x, y, width, height, testName, future)
+                        } else {
+                            future.complete(replyMessage)
+                        }
                     } catch (e: Exception) {
                         future.completeExceptionally(e)
                     }
@@ -72,18 +96,18 @@
                 if (!future.isDone) {
                     Log.w(
                         TAG,
-                        "Rendering '$testName' is taking longer than expected (exceeded 5 seconds)..."
+                        "Rendering '$testName' is taking longer than expected (exceeded $DIAGNOSTIC_WARNING_DELAY_SEC seconds)..."
                     )
                 }
             },
-            5,
+            DIAGNOSTIC_WARNING_DELAY_SEC,
             TimeUnit.SECONDS
         )
 
         val reply: String
         try {
-            // Wait with a very generous 60-second timeout to catch true deadlocks/crashes
-            reply = future.get(60, TimeUnit.SECONDS)
+            // Wait with a very generous timeout to catch true deadlocks/crashes
+            reply = future.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS)
         } catch (e: Exception) {
             Log.e(TAG, "$testName Failed to receive result on message channel: ${e.message}")
             throw RuntimeException(e)
@@ -92,7 +116,88 @@
         }
 
         Log.d(TAG, "Received $reply on message channel")
-        assertEquals("Rendered $testName", reply)
+        if (testName == PLATFORM_VIEW_TEST_NAME) {
+            assertEquals("Comparison Success", reply)
+        } else {
+            assertEquals("Rendered $testName", reply)
+        }
+    }
+
+    private fun captureAndSendScreenshot(
+        x: Int,
+        y: Int,
+        width: Int,
+        height: Int,
+        testName: String,
+        future: CompletableFuture<String>
+    ) {
+        // Capture the screenshot on a background thread with a short delay. We must NOT sleep or capture
+        // on the Main UI Thread to avoid blocking frame rendering or causing an ANR.
+        val screenshotExecutor = Executors.newSingleThreadScheduledExecutor()
+        screenshotExecutor.schedule({
+            try {
+                // Capture the true screen output using UiAutomation from this privileged instrumentation runner process.
+                val instrumentation = InstrumentationRegistry.getInstrumentation()
+                val screenshot =
+                    instrumentation.uiAutomation.takeScreenshot()
+                        ?: throw IllegalStateException("UiAutomation.takeScreenshot() returned null")
+
+                if (x < 0 ||
+                    y < 0 ||
+                    width <= 0 ||
+                    height <= 0 ||
+                    x + width > screenshot.width ||
+                    y + height > screenshot.height
+                ) {
+                    throw IllegalArgumentException(
+                        "Crop bounds out of range: x=$x, y=$y, width=$width, height=$height, screenshot.width=${screenshot.width}, screenshot.height=${screenshot.height}"
+                    )
+                }
+
+                // Crop the full-screen screenshot to the exact widget bounds.
+                val cropped = Bitmap.createBitmap(screenshot, x, y, width, height)
+                if (cropped != screenshot) {
+                    screenshot.recycle()
+                }
+
+                val stream = ByteArrayOutputStream()
+                try {
+                    cropped.compress(Bitmap.CompressFormat.PNG, 100, stream)
+                } finally {
+                    cropped.recycle()
+                }
+                val croppedBytes = stream.toByteArray()
+                val base64Image = Base64.encodeToString(croppedBytes, Base64.NO_WRAP)
+
+                val compareMsg =
+                    JSONObject().apply {
+                        put("command", "compare_golden")
+                        put("testName", testName)
+                        put("imageBytes", base64Image)
+                    }
+
+                Log.d(TAG, "Sending compare_golden request to Dart app")
+                // Send the cropped PNG bytes back to Dart so all golden comparisons are resolved via Dart's matchesGoldenFile.
+                rule.scenario.onActivity { mainActivity ->
+                    mainActivity.messageChannel?.send(compareMsg) { compareReply ->
+                        try {
+                            val compareReplyJson =
+                                compareReply as? JSONObject
+                                    ?: throw IllegalStateException(
+                                        "Expected JSONObject reply from compare_golden request, but received: $compareReply"
+                                    )
+                            future.complete(compareReplyJson.getString("message"))
+                        } catch (e: Exception) {
+                            future.completeExceptionally(e)
+                        }
+                    }
+                }
+            } catch (e: Exception) {
+                future.completeExceptionally(e)
+            } finally {
+                screenshotExecutor.shutdown()
+            }
+        }, SCREENSHOT_CAPTURE_DELAY_MS, TimeUnit.MILLISECONDS)
     }
 
     @Test
@@ -124,4 +229,9 @@
     fun backdropFilterBlurTest() {
         templateTest("backdropFilterBlurTest")
     }
+
+    @Test
+    fun platformViewTest() {
+        templateTest(PLATFORM_VIEW_TEST_NAME)
+    }
 }
diff --git a/dev/integration_tests/android_hardware_smoke_test/android/app/src/main/kotlin/com/example/android_hardware_smoke_test/MainActivity.kt b/dev/integration_tests/android_hardware_smoke_test/android/app/src/main/kotlin/com/example/android_hardware_smoke_test/MainActivity.kt
index 6ad5c20..7217fab 100644
--- a/dev/integration_tests/android_hardware_smoke_test/android/app/src/main/kotlin/com/example/android_hardware_smoke_test/MainActivity.kt
+++ b/dev/integration_tests/android_hardware_smoke_test/android/app/src/main/kotlin/com/example/android_hardware_smoke_test/MainActivity.kt
@@ -1,6 +1,9 @@
+@file:Suppress("PackageName")
+
 package com.example.android_hardware_smoke_test
 
 import android.content.pm.PackageManager
+import android.os.Build
 import android.util.Log
 import io.flutter.embedding.android.FlutterActivity
 import io.flutter.embedding.engine.FlutterEngine
@@ -42,6 +45,14 @@
                 JSONMessageCodec.INSTANCE
             )
 
+        flutterEngine
+            .platformViewsController
+            .registry
+            .registerViewFactory(
+                "com.example.android_hardware_smoke_test/native_text_view",
+                NativeTextViewFactory()
+            )
+
         MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL_NAME)
             .setMethodCallHandler { call, result ->
                 if (call.method == "impeller_backend") {
@@ -50,5 +61,29 @@
                     result.notImplemented()
                 }
             }
+
+        // Register the native_driver channel. This responds to AndroidNativeDriver's connection ping
+        // and property checks, enabling the host-side runner to take compositor-level screenshots via ADB.
+        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "native_driver")
+            .setMethodCallHandler { call, result ->
+                when (call.method) {
+                    "sdk_version" -> {
+                        result.success(mapOf("version" to Build.VERSION.SDK_INT))
+                    }
+                    "is_emulator" -> {
+                        val isEmulator =
+                            Build.MODEL.contains("gphone") ||
+                                Build.MODEL.contains("Emulator") ||
+                                Build.MODEL.contains("Android SDK built for x86")
+                        result.success(mapOf("emulator" to isEmulator))
+                    }
+                    "ping" -> {
+                        result.success(null)
+                    }
+                    else -> {
+                        result.notImplemented()
+                    }
+                }
+            }
     }
 }
diff --git a/dev/integration_tests/android_hardware_smoke_test/android/app/src/main/kotlin/com/example/android_hardware_smoke_test/NativeTextView.kt b/dev/integration_tests/android_hardware_smoke_test/android/app/src/main/kotlin/com/example/android_hardware_smoke_test/NativeTextView.kt
new file mode 100644
index 0000000..fd341d5
--- /dev/null
+++ b/dev/integration_tests/android_hardware_smoke_test/android/app/src/main/kotlin/com/example/android_hardware_smoke_test/NativeTextView.kt
@@ -0,0 +1,42 @@
+@file:Suppress("PackageName")
+
+package com.example.android_hardware_smoke_test
+
+import android.content.Context
+import android.graphics.Color
+import android.view.Gravity
+import android.view.View
+import android.widget.TextView
+import io.flutter.plugin.common.StandardMessageCodec
+import io.flutter.plugin.platform.PlatformView
+import io.flutter.plugin.platform.PlatformViewFactory
+
+class NativeTextViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
+    override fun create(
+        context: Context,
+        viewId: Int,
+        args: Any?
+    ): PlatformView {
+        val creationParams = args as? Map<String, Any?>
+        return NativeTextView(context, viewId, creationParams)
+    }
+}
+
+class NativeTextView(
+    context: Context,
+    id: Int,
+    creationParams: Map<String, Any?>?
+) : PlatformView {
+    private val textView: TextView =
+        TextView(context).apply {
+            textSize = 22f
+            gravity = Gravity.CENTER
+            setBackgroundColor(Color.rgb(100, 200, 255)) // Light blue background
+            text = creationParams?.get("text") as? String ?: "Default Native Text"
+            setTextColor(Color.BLACK)
+        }
+
+    override fun getView(): View = textView
+
+    override fun dispose() {}
+}
diff --git a/dev/integration_tests/android_hardware_smoke_test/integration_test/integration_test_wrapper.dart b/dev/integration_tests/android_hardware_smoke_test/integration_test/integration_test_wrapper.dart
index 41f07ec..dc4b697 100644
--- a/dev/integration_tests/android_hardware_smoke_test/integration_test/integration_test_wrapper.dart
+++ b/dev/integration_tests/android_hardware_smoke_test/integration_test/integration_test_wrapper.dart
@@ -5,6 +5,7 @@
 import 'dart:async';
 import 'dart:convert';
 
+import 'package:android_driver_extensions/extension.dart';
 import 'package:android_hardware_smoke_test/main.dart' as app;
 import 'package:flutter/services.dart';
 import 'package:flutter_driver/driver_extension.dart';
@@ -13,14 +14,20 @@
   const channelName = 'com.example.android_hardware_smoke_test/test_channel';
 
   enableFlutterDriverExtension(
+    // Register nativeDriverCommands so that host-side driver extensions can send NativeCommands
+    // (specifically for capturing system-level screenshots from the host during host-driven tests).
+    commands: <CommandExtension>[nativeDriverCommands],
     // Thin handler to bridge driver's requestData and MainApp's test_channel.
     handler: (String? request) async {
       if (request == null) {
-        return json.encode(<String, Object?>{'message': 'Error: request was null'});
+        return json.encode(<String, Object?>{
+          'message': 'Error: request was null',
+        });
       }
 
-      final Map<String, Object?> payload = (json.decode(request) as Map<Object?, Object?>)
-          .cast<String, Object?>();
+      final Map<String, Object?> payload =
+          (json.decode(request) as Map<Object?, Object?>)
+              .cast<String, Object?>();
 
       // Handle host-side graphics backend self-discovery query
       if (payload['command'] == 'get_golden_variant') {
@@ -35,12 +42,16 @@
       final completer = Completer<String>();
 
       // ignore: deprecated_member_use
-      ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(channelName, message, (
-        ByteData? replyData,
-      ) {
-        final reply = const JSONMessageCodec().decodeMessage(replyData) as Map<Object?, Object?>?;
-        completer.complete(json.encode(reply));
-      });
+      ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
+        channelName,
+        message,
+        (ByteData? replyData) {
+          final reply =
+              const JSONMessageCodec().decodeMessage(replyData)
+                  as Map<Object?, Object?>?;
+          completer.complete(json.encode(reply));
+        },
+      );
 
       return completer.future;
     },
diff --git a/dev/integration_tests/android_hardware_smoke_test/lib/goldens.dart b/dev/integration_tests/android_hardware_smoke_test/lib/goldens.dart
index 9fd54d3..7ea84a6 100644
--- a/dev/integration_tests/android_hardware_smoke_test/lib/goldens.dart
+++ b/dev/integration_tests/android_hardware_smoke_test/lib/goldens.dart
@@ -14,25 +14,81 @@
 import 'package:path/path.dart' as path;
 import 'package:path_provider/path_provider.dart';
 
+import 'pixel_exact_local_file_comparator.dart';
+
 /// Captures the image bytes of the widget associated with [targetKey] and either compares it to a golden file or returns the bytes to the test driver for host-side comparison, depending on the value of [performAppSideGoldenCompare].
+///
+/// The optional [settleFuture] parameter allows injecting an asynchronous
+/// initialization or layout completion signal (such as a platform view creation
+/// event). If provided, execution will await [settleFuture] before capturing
+/// physical screen coordinates.
 Future<void> handleGoldenRequest(
   String testName,
   Completer<Map<String, Object?>> completer,
   bool performAppSideGoldenCompare,
   GlobalKey targetKey,
-  Future<String?> goldenVariant,
-) async {
+  Future<String?> goldenVariant, {
+  Future<void>? settleFuture,
+}) async {
   try {
     final String? goldenVariantValue = await goldenVariant;
+
+    if (testName == 'platformViewTest') {
+      // Platform views cannot be captured using RepaintBoundary.toImage() since they reside in separate
+      // native surface layers. Instead, we wait for layout to settle, calculate the widget's physical
+      // coordinates on screen, and return them so the runner can perform a compositor-level capture.
+      if (settleFuture != null) {
+        await settleFuture;
+      }
+      for (int i = 0; i < 3; i++) {
+        await WidgetsBinding.instance.endOfFrame;
+      }
+
+      final BuildContext? context = targetKey.currentContext;
+      if (context == null || !context.mounted) {
+        throw StateError(
+          'Failed to capture coordinates for $testName: targetKey is not mounted in the widget tree.',
+        );
+      }
+      final RenderObject? renderObject = context.findRenderObject();
+      if (renderObject is! RenderBox) {
+        throw StateError(
+          'Failed to capture coordinates for $testName: the associated RenderObject is not a RenderBox.',
+        );
+      }
+
+      final Offset position = renderObject.localToGlobal(Offset.zero);
+      final Size size = renderObject.size;
+      // We can assume one window for these tests since they are android-only.
+      final double devicePixelRatio =
+          ui.PlatformDispatcher.instance.views.first.devicePixelRatio;
+
+      final int x = (position.dx * devicePixelRatio).round();
+      final int y = (position.dy * devicePixelRatio).round();
+      final int w = (size.width * devicePixelRatio).round();
+      final int h = (size.height * devicePixelRatio).round();
+
+      completer.complete(<String, Object?>{
+        'message': 'Rendered $testName',
+        'x': x,
+        'y': y,
+        'width': w,
+        'height': h,
+      });
+      return;
+    }
+
     final Uint8List resultImageBytes = await _capturePng(testName, targetKey);
 
     if (performAppSideGoldenCompare) {
-      final String? failureMessage = await _compareGoldenOnDevice(
+      final String? failureMessage = await compareGoldenOnDevice(
         testName,
         resultImageBytes,
         goldenVariantValue,
       );
-      completer.complete(<String, Object?>{'message': failureMessage ?? 'Rendered $testName'});
+      completer.complete(<String, Object?>{
+        'message': failureMessage ?? 'Rendered $testName',
+      });
     } else {
       completer.complete(<String, Object?>{
         'message': 'Rendered $testName',
@@ -42,16 +98,24 @@
   } catch (e, stackTrace) {
     // Guarantee that the completer completes even under unhandled exceptions
     completer.complete(<String, Object?>{
-      'message': 'Error occurred during golden request handling: $e\n$stackTrace',
+      'message':
+          'Error occurred during golden request handling: $e\n$stackTrace',
     });
   }
 }
 
-Future<String?> _compareGoldenOnDevice(
+/// Compares [resultImageBytes] against the golden asset associated with
+/// [testName] and an optional [goldenVariant] on the device.
+///
+/// Returns an error message string if the comparison fails, or `null` if the
+/// image matches the golden perfectly.
+Future<String?> compareGoldenOnDevice(
   String testName,
   Uint8List resultImageBytes,
   String? goldenVariant,
 ) async {
+  goldenFileComparator = const PixelExactLocalFileComparator();
+
   final io.Directory tempDir = await getTemporaryDirectory();
   final variantSuffix = (goldenVariant != null && goldenVariant.isNotEmpty)
       ? '.$goldenVariant'
@@ -83,7 +147,10 @@
   await file.writeAsBytes(bytes);
 }
 
-Future<void> _copyGoldenAssetToTemp(String goldenAssetPath, String tempGoldenPath) async {
+Future<void> _copyGoldenAssetToTemp(
+  String goldenAssetPath,
+  String tempGoldenPath,
+) async {
   try {
     final ByteData byteData = await rootBundle.load(goldenAssetPath);
     final Uint8List bytes = byteData.buffer.asUint8List(
@@ -108,6 +175,12 @@
   }
 
   final RenderObject? renderObject = context.findRenderObject();
+  if (renderObject == null) {
+    throw StateError(
+      'Failed to capture screenshot for $testName: the associated RenderObject is null.',
+    );
+  }
+
   if (renderObject is! RenderRepaintBoundary) {
     throw StateError(
       'Failed to capture screenshot for $testName: the associated RenderObject is not a RenderRepaintBoundary.',
@@ -116,7 +189,9 @@
   final RenderRepaintBoundary boundary = renderObject;
   final ui.Image image = await boundary.toImage(pixelRatio: 3.0);
   try {
-    final ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
+    final ByteData? byteData = await image.toByteData(
+      format: ui.ImageByteFormat.png,
+    );
     if (byteData == null) {
       throw StateError(
         'Failed to capture screenshot for $testName: ui.Image.toByteData returned null.',
diff --git a/dev/integration_tests/android_hardware_smoke_test/lib/main.dart b/dev/integration_tests/android_hardware_smoke_test/lib/main.dart
index 8b5124b..498a3ec 100644
--- a/dev/integration_tests/android_hardware_smoke_test/lib/main.dart
+++ b/dev/integration_tests/android_hardware_smoke_test/lib/main.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:ui' as ui;
 
 import 'package:flutter/material.dart';
@@ -11,6 +12,7 @@
 import 'backdrop_filter_blur.dart';
 import 'goldens.dart';
 import 'image_drawing_canvas.dart';
+import 'platform_view.dart';
 import 'text_drawing_canvas.dart';
 import 'vector_drawings_canvas.dart';
 
@@ -32,15 +34,13 @@
   Widget build(BuildContext context) {
     return MaterialApp(
       title: 'Flutter android hardware smoke test',
-      theme: ThemeData(
-        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
-      ),
+      theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)),
       home: MyWidget(imageLoader: imageLoader),
     );
   }
 }
 
-/// A stateful widget rendering vector drawings or blur effects based on test driver requests.
+/// A stateful widget rendering vector drawings, blur effects, or platform views based on test driver requests.
 class MyWidget extends StatefulWidget {
   const MyWidget({super.key, required this.imageLoader});
 
@@ -64,6 +64,7 @@
   String _message = 'Waiting for message...';
   late Future<String?> _goldenVariantFuture;
   ui.Image? _loadedImage;
+  Completer<void>? _platformViewCreatedCompleter;
 
   Future<ui.Image> _loadImage() async {
     return widget.imageLoader();
@@ -72,6 +73,24 @@
   Future<Map<String, Object?>?> _handler(Object? message) async {
     final Map<String, Object?>? messageMap = (message as Map<Object?, Object?>?)
         ?.cast<String, Object?>();
+
+    if (messageMap?['command'] == 'compare_golden') {
+      // Handle the out-of-band comparison request. This is triggered by the on-device test runner
+      // once it has captured and cropped the platform view screenshot using UiAutomation.
+      final testName = messageMap!['testName']! as String;
+      final imageBase64 = messageMap['imageBytes']! as String;
+      final Uint8List imageBytes = base64.decode(imageBase64);
+      final String? goldenVariantValue = await _goldenVariantFuture;
+
+      final String? failureMessage = await compareGoldenOnDevice(
+        testName,
+        imageBytes,
+        goldenVariantValue,
+      );
+
+      return <String, Object?>{'message': failureMessage ?? 'Comparison Success'};
+    }
+
     final testName = messageMap?['testName'] as String?;
     final bool performAppSideGoldenCompare =
         messageMap?['performAppSideGoldenCompare'] as bool? ?? true;
@@ -79,8 +98,7 @@
     // Widget tests pass captureScreenshot: false.
     // Image.toByteData runs async on a native thread, which results in an unresolvable deadlock in the widget test's FakeAsync zone.
     // Comparing pixels is not a responsibility of widget tests anyway, that should be reserved for the integration tests.
-    final bool captureScreenshot =
-        messageMap?['captureScreenshot'] as bool? ?? true;
+    final bool captureScreenshot = messageMap?['captureScreenshot'] as bool? ?? true;
 
     // Lazily load the image asset only when requested. This avoids loading it
     // unnecessarily, blocks rendering until fully loaded, and catches load
@@ -101,14 +119,18 @@
           _loadedImage = img;
         });
       } catch (e, stackTrace) {
-        return <String, Object?>{
-          'message': 'Failed to load image asset: $e\n$stackTrace',
-        };
+        return <String, Object?>{'message': 'Failed to load image asset: $e\n$stackTrace'};
       }
     }
 
     final completer = Completer<Map<String, Object?>>();
 
+    if (testName == 'platformViewTest') {
+      _platformViewCreatedCompleter = Completer<void>();
+    } else {
+      _platformViewCreatedCompleter = null;
+    }
+
     setState(() {
       _message = testName ?? 'Empty message';
     });
@@ -121,12 +143,10 @@
           performAppSideGoldenCompare,
           targetKey,
           _goldenVariantFuture,
+          settleFuture: _platformViewCreatedCompleter?.future,
         );
       } else {
-        completer.complete(<String, Object?>{
-          'message': 'Rendered $testName',
-          'imageBytes': null,
-        });
+        completer.complete(<String, Object?>{'message': 'Rendered $testName', 'imageBytes': null});
       }
     }, debugLabel: 'Rendered $testName');
 
@@ -137,9 +157,7 @@
   void initState() {
     super.initState();
 
-    _goldenVariantFuture = _nativeChannel.invokeMethod<String>(
-      'impeller_backend',
-    );
+    _goldenVariantFuture = _nativeChannel.invokeMethod<String>('impeller_backend');
     _testChannel.setMessageHandler(_handler);
   }
 
@@ -152,16 +170,19 @@
 
   @override
   Widget build(BuildContext context) {
-    final Widget testContent;
-    if (_message == 'backdropFilterBlurTest') {
-      testContent = const BackdropFilterBlur();
-    } else if (_message == 'textTest') {
-      testContent = const TextDrawingCanvas();
-    } else if (_message == 'imageTest') {
-      testContent = ImageDrawingCanvas(image: _loadedImage);
-    } else {
-      testContent = VectorDrawingsCanvas(message: _message);
-    }
+    final Widget testContent = switch (_message) {
+      'backdropFilterBlurTest' => const BackdropFilterBlur(),
+      'platformViewTest' => AndroidPlatformView(
+        onCreated: () {
+          if (_platformViewCreatedCompleter?.isCompleted == false) {
+            _platformViewCreatedCompleter?.complete();
+          }
+        },
+      ),
+      'textTest' => const TextDrawingCanvas(),
+      'imageTest' => ImageDrawingCanvas(image: _loadedImage),
+      _ => VectorDrawingsCanvas(message: _message),
+    };
 
     return SafeArea(
       child: Stack(
diff --git a/dev/integration_tests/android_hardware_smoke_test/lib/pixel_exact_local_file_comparator.dart b/dev/integration_tests/android_hardware_smoke_test/lib/pixel_exact_local_file_comparator.dart
new file mode 100644
index 0000000..4190630
--- /dev/null
+++ b/dev/integration_tests/android_hardware_smoke_test/lib/pixel_exact_local_file_comparator.dart
@@ -0,0 +1,75 @@
+// Copyright 2014 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:io' as io;
+import 'dart:typed_data';
+import 'dart:ui' as ui;
+
+import 'package:android_driver_extensions/native_driver.dart'
+    show GoldenFileComparator;
+
+/// We use a raw pixel-exact comparator on-device because comparing compressed PNG files byte-for-byte
+/// is flaky. Android's native zlib compressor (libpng) and Dart's pure-Dart 'image' encoder use
+/// different metadata, chunk orderings, and zlib compression levels for identical images.
+class PixelExactLocalFileComparator extends GoldenFileComparator {
+  const PixelExactLocalFileComparator();
+
+  @override
+  Future<bool> compare(Uint8List imageBytes, Uri golden) async {
+    final goldenFile = io.File(golden.toFilePath());
+    if (!goldenFile.existsSync()) {
+      throw StateError('Golden file not found: ${goldenFile.path}');
+    }
+    final Uint8List goldenBytes = await goldenFile.readAsBytes();
+
+    // Decode both images to raw pixels
+    final ui.Image image1 = await _decodePng(imageBytes);
+    final ui.Image image2 = await _decodePng(goldenBytes);
+
+    try {
+      if (image1.width != image2.width || image1.height != image2.height) {
+        return false;
+      }
+
+      final ByteData? bytes1 = await image1.toByteData();
+      final ByteData? bytes2 = await image2.toByteData();
+
+      if (bytes1 == null || bytes2 == null) {
+        return false;
+      }
+
+      if (bytes1.lengthInBytes != bytes2.lengthInBytes) {
+        return false;
+      }
+
+      final Uint8List list1 = bytes1.buffer.asUint8List();
+      final Uint8List list2 = bytes2.buffer.asUint8List();
+
+      for (var i = 0; i < list1.length; i++) {
+        if (list1[i] != list2[i]) {
+          return false;
+        }
+      }
+
+      return true;
+    } finally {
+      image1.dispose();
+      image2.dispose();
+    }
+  }
+
+  Future<ui.Image> _decodePng(Uint8List bytes) async {
+    final ui.Codec codec = await ui.instantiateImageCodec(bytes);
+    final ui.FrameInfo fi = await codec.getNextFrame();
+    codec.dispose();
+    return fi.image;
+  }
+
+  @override
+  Future<void> update(Uri golden, Uint8List imageBytes) async {
+    final goldenFile = io.File(golden.toFilePath());
+    await goldenFile.parent.create(recursive: true);
+    await goldenFile.writeAsBytes(imageBytes);
+  }
+}
diff --git a/dev/integration_tests/android_hardware_smoke_test/lib/platform_view.dart b/dev/integration_tests/android_hardware_smoke_test/lib/platform_view.dart
new file mode 100644
index 0000000..279cf89
--- /dev/null
+++ b/dev/integration_tests/android_hardware_smoke_test/lib/platform_view.dart
@@ -0,0 +1,83 @@
+// Copyright 2014 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/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/services.dart';
+
+/// A custom widget embedding a native Android TextView inside the Flutter
+/// layout hierarchy using Hybrid Composition and drawing a Flutter overlay on top.
+class AndroidPlatformView extends StatelessWidget {
+  const AndroidPlatformView({super.key, this.onCreated});
+
+  final VoidCallback? onCreated;
+
+  @override
+  Widget build(BuildContext context) {
+    const viewType = 'com.example.android_hardware_smoke_test/native_text_view';
+    const creationParams = <String, dynamic>{
+      'text': 'Native\n🐞 View 🪲\nContent ',
+    };
+
+    return Stack(
+      children: <Widget>[
+        Positioned.fill(
+          child: PlatformViewLink(
+            viewType: viewType,
+            surfaceFactory:
+                (BuildContext context, PlatformViewController controller) {
+                  return AndroidViewSurface(
+                    controller: controller as AndroidViewController,
+                    gestureRecognizers:
+                        const <Factory<OneSequenceGestureRecognizer>>{},
+                    hitTestBehavior: PlatformViewHitTestBehavior.opaque,
+                  );
+                },
+            onCreatePlatformView: (PlatformViewCreationParams params) {
+              return PlatformViewsService.initAndroidView(
+                  id: params.id,
+                  viewType: viewType,
+                  layoutDirection: TextDirection.ltr,
+                  creationParams: creationParams,
+                  creationParamsCodec: const StandardMessageCodec(),
+                  onFocus: () => params.onFocusChanged(true),
+                )
+                ..addOnPlatformViewCreatedListener((int id) {
+                  params.onPlatformViewCreated(id);
+                  onCreated?.call();
+                })
+                ..create();
+            },
+          ),
+        ),
+        Center(
+          child: Container(
+            width: 120,
+            height: 90,
+            decoration: BoxDecoration(
+              color: Colors.red.withValues(
+                alpha: 0.6,
+              ), // Semi-transparent overlay
+              shape: BoxShape.circle,
+              border: Border.all(color: Colors.white, width: 3.0),
+            ),
+            alignment: Alignment.center,
+            child: const Text(
+              'Flutter\n🐦🐦🐦',
+              textAlign: TextAlign.center,
+              style: TextStyle(
+                color: Colors.white,
+                fontSize: 14.0,
+                fontWeight: FontWeight.bold,
+                decoration: TextDecoration.none,
+              ),
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
diff --git a/dev/integration_tests/android_hardware_smoke_test/pubspec.yaml b/dev/integration_tests/android_hardware_smoke_test/pubspec.yaml
index 74ee346..b375b9f 100644
--- a/dev/integration_tests/android_hardware_smoke_test/pubspec.yaml
+++ b/dev/integration_tests/android_hardware_smoke_test/pubspec.yaml
@@ -23,6 +23,7 @@
   flutter_test:
     sdk: flutter
   test: any
+  image: any
 
   # The "flutter_lints" package below contains a set of recommended lints to
   # encourage good coding practices. The lint set provided by the package is
@@ -43,4 +44,4 @@
     - test_driver/goldens/
 
 
-# PUBSPEC CHECKSUM: vehlaq
\ No newline at end of file
+# PUBSPEC CHECKSUM: tqljka
\ No newline at end of file
diff --git a/dev/integration_tests/android_hardware_smoke_test/test/widget_test.dart b/dev/integration_tests/android_hardware_smoke_test/test/widget_test.dart
index 3ecf51b..c61c167 100644
--- a/dev/integration_tests/android_hardware_smoke_test/test/widget_test.dart
+++ b/dev/integration_tests/android_hardware_smoke_test/test/widget_test.dart
@@ -6,6 +6,7 @@
 
 import 'package:android_hardware_smoke_test/image_drawing_canvas.dart';
 import 'package:android_hardware_smoke_test/main.dart';
+import 'package:android_hardware_smoke_test/platform_view.dart';
 import 'package:android_hardware_smoke_test/text_drawing_canvas.dart';
 import 'package:android_hardware_smoke_test/vector_drawings_canvas.dart';
 import 'package:flutter/material.dart';
@@ -226,4 +227,37 @@
     // Verify BackdropFilter widget is present in the tree
     expect(find.byType(BackdropFilter), findsOneWidget);
   });
+
+  testWidgets('platformViewTest message channel handler - success behavior', (
+    WidgetTester tester,
+  ) async {
+    await tester.pumpWidget(const MyApp());
+
+    final ByteData encodedMessage = const JSONMessageCodec().encodeMessage(<String, Object?>{
+      'testName': 'platformViewTest',
+      'performAppSideGoldenCompare': false,
+      'captureScreenshot': false,
+    })!;
+
+    final Future<ByteData?> responseFuture = TestDefaultBinaryMessengerBinding
+        .instance
+        .defaultBinaryMessenger
+        .handlePlatformMessage(
+          'com.example.android_hardware_smoke_test/test_channel',
+          encodedMessage,
+          null,
+        );
+
+    await tester.pump();
+    await tester.pump();
+
+    final ByteData? responseBytes = await responseFuture;
+    expect(responseBytes, isNotNull);
+    final dynamic reply = const JSONMessageCodec().decodeMessage(responseBytes);
+    final Map<String, Object?> replyMap = (reply as Map<Object?, Object?>).cast<String, Object?>();
+    expect(replyMap['message'], equals('Rendered platformViewTest'));
+
+    // Verify AndroidPlatformView widget is present in the tree
+    expect(find.byType(AndroidPlatformView), findsOneWidget);
+  });
 }
diff --git a/dev/integration_tests/android_hardware_smoke_test/test_driver/driver_test.dart b/dev/integration_tests/android_hardware_smoke_test/test_driver/driver_test.dart
index 166a1f5..7f83c11 100644
--- a/dev/integration_tests/android_hardware_smoke_test/test_driver/driver_test.dart
+++ b/dev/integration_tests/android_hardware_smoke_test/test_driver/driver_test.dart
@@ -9,17 +9,23 @@
 import 'package:android_driver_extensions/native_driver.dart';
 import 'package:android_driver_extensions/skia_gold.dart';
 import 'package:flutter_driver/flutter_driver.dart';
+import 'package:image/image.dart' as img;
 import 'package:test/test.dart';
 
 /// Whether the current environment is LUCI.
 bool get isLuci => io.Platform.environment['LUCI_CI'] == 'True';
 
+const String platformViewTestName = 'platformViewTest';
+
 void main() async {
   late final FlutterDriver flutterDriver;
+  late final NativeDriver nativeDriver;
   late final String activeGoldenVariant;
 
   setUpAll(() async {
     flutterDriver = await FlutterDriver.connect();
+    nativeDriver = await AndroidNativeDriver.connect(flutterDriver);
+    await nativeDriver.configureForScreenshotTesting();
 
     final String response = await flutterDriver.requestData(
       json.encode(<String, Object?>{'command': 'get_golden_variant'}),
@@ -40,6 +46,7 @@
   });
 
   tearDownAll(() async {
+    await nativeDriver.close();
     await flutterDriver.close();
   });
 
@@ -58,9 +65,46 @@
             .cast<String, Object?>();
     expect(reply['message'], equals('Rendered $testName'));
 
+    final Uint8List imageBytes;
+    if (testName == platformViewTestName) {
+      final int x = reply['x']! as int;
+      final int y = reply['y']! as int;
+      final int w = reply['width']! as int;
+      final int h = reply['height']! as int;
+
+      final NativeScreenshot fullScreenshot = await nativeDriver.screenshot();
+      final Uint8List fullBytes = await fullScreenshot.readAsBytes();
+
+      final img.Image? decoded = img.decodePng(fullBytes);
+      if (decoded == null) {
+        throw StateError(
+          'Failed to decode full screen screenshot for $testName',
+        );
+      }
+      if (x < 0 ||
+          y < 0 ||
+          w <= 0 ||
+          h <= 0 ||
+          x + w > decoded.width ||
+          y + h > decoded.height) {
+        throw StateError(
+          'Crop bounds out of range for $testName: x=$x, y=$y, w=$w, h=$h, image.width=${decoded.width}, image.height=${decoded.height}',
+        );
+      }
+      final img.Image cropped = img.copyCrop(
+        decoded,
+        x: x,
+        y: y,
+        width: w,
+        height: h,
+      );
+      imageBytes = Uint8List.fromList(img.encodePng(cropped));
+    } else {
+      final imageBase64 = reply['imageBytes']! as String;
+      imageBytes = base64.decode(imageBase64);
+    }
+
     // Compare the bytes to a golden file on the host filesystem using the cached variant
-    final imageBase64 = reply['imageBytes']! as String;
-    final Uint8List imageBytes = base64.decode(imageBase64);
     await expectLater(
       imageBytes,
       matchesGoldenFile('goldens/$testName$activeGoldenVariant.png'),
@@ -90,4 +134,8 @@
   test('should render and match backdropFilterBlurTest golden', () async {
     await templateTest('backdropFilterBlurTest');
   }, timeout: Timeout.none);
+
+  test('should render and match $platformViewTestName golden', () async {
+    await templateTest(platformViewTestName);
+  }, timeout: Timeout.none);
 }