From 772f046cbc77834ada335ef65015d32405a65066 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 1 Apr 2026 12:05:17 -0400 Subject: [PATCH 1/4] test: screenshot plain Android view in IsolatedTest Verifies the screenshot infrastructure works without React Native. Co-Authored-By: Claude Sonnet 4.6 --- .../src/androidTest/java/com/testapp/IsolatedTest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index bcd300f..97054e5 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -1,7 +1,9 @@ package com.rnstorybookautoscreenshots +import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.facebook.testing.screenshot.Screenshot import com.testapp.MainApplication import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue @@ -23,4 +25,13 @@ class IsolatedTest { surface.start() assertNotNull(surface.view) } + + @Test + fun screenshotPlainView() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val view = View(context).also { + it.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + Screenshot.snap(view).setName("plain_view").record() + } } From 3704bbbe15facc02624569eca3daf44190302159 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 1 Apr 2026 12:12:20 -0400 Subject: [PATCH 2/4] fix: measure and layout plain view before screenshotting Co-Authored-By: Claude Sonnet 4.6 --- android/app/src/androidTest/java/com/testapp/IsolatedTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index 97054e5..edaff38 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -32,6 +32,11 @@ class IsolatedTest { val view = View(context).also { it.setLayerType(View.LAYER_TYPE_SOFTWARE, null) } + view.measure( + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + ) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) Screenshot.snap(view).setName("plain_view").record() } } From 550baf640d7445034fb4ae8560e0ad76fdc10412 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 1 Apr 2026 18:54:15 -0400 Subject: [PATCH 3/4] test: screenshot SimpleTestComponent React Native surface in IsolatedTest Replaces the plain Android view screenshot with an actual React Native surface screenshot, which is the real goal of this isolated test. Uses the same WindowManager + software layer approach as BaseStoryScreenshotTest so Fabric can commit its render tree in a test process. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/testapp/IsolatedTest.kt | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index edaff38..2c94c93 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -1,17 +1,28 @@ package com.rnstorybookautoscreenshots +import android.Manifest +import android.graphics.PixelFormat import android.view.View +import android.view.WindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule import com.facebook.testing.screenshot.Screenshot import com.testapp.MainApplication import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class IsolatedTest { + + @get:Rule + val permissionRule: GrantPermissionRule = GrantPermissionRule.grant( + Manifest.permission.SYSTEM_ALERT_WINDOW + ) + @Test fun simpleTest() { assertTrue(true) @@ -27,16 +38,38 @@ class IsolatedTest { } @Test - fun screenshotPlainView() { - val context = InstrumentationRegistry.getInstrumentation().targetContext - val view = View(context).also { - it.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + fun screenshotSimpleTestComponent() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val context = instrumentation.targetContext + val app = context.applicationContext as MainApplication + + val surface = app.reactHost.createSurface(context, "SimpleTestComponent", null) + val view = surface.view + ?: throw IllegalStateException("ReactSurface returned a null view") + + val wm = context.getSystemService(android.content.Context.WINDOW_SERVICE) as WindowManager + val params = WindowManager.LayoutParams( + 1080, + 1920, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT + ).apply { alpha = 0f } + + instrumentation.runOnMainSync { + view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + wm.addView(view, params) + surface.start() + } + + // Wait for Fabric to commit its render tree + Thread.sleep(500) + + Screenshot.snap(view).setName("simple_test_component").record() + + instrumentation.runOnMainSync { + surface.stop() + wm.removeView(view) } - view.measure( - View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) - ) - view.layout(0, 0, view.measuredWidth, view.measuredHeight) - Screenshot.snap(view).setName("plain_view").record() } } From 0bebcca6cf6fd3733328b8a6343e683071eba89f Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 1 Apr 2026 21:39:24 -0400 Subject: [PATCH 4/4] fix: use proper Fabric wait and recursive software layer in screenshotSimpleTestComponent - Use ContextThemeWrapper (matches BaseStoryScreenshotTest pattern) - Poll childCount on main thread with 30s timeout to handle cold JS bundle load - Set software layer recursively after Fabric mounts children (child views default to hardware acceleration which Screenshot.snap() can't capture) - Call Screenshot.snap() on the main thread Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/testapp/IsolatedTest.kt | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index 2c94c93..4dd336b 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -2,7 +2,12 @@ package com.rnstorybookautoscreenshots import android.Manifest import android.graphics.PixelFormat +import android.os.Handler +import android.os.Looper +import android.view.Choreographer +import android.view.ContextThemeWrapper import android.view.View +import android.view.ViewGroup import android.view.WindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry @@ -14,6 +19,7 @@ import junit.framework.TestCase.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) class IsolatedTest { @@ -43,7 +49,8 @@ class IsolatedTest { val context = instrumentation.targetContext val app = context.applicationContext as MainApplication - val surface = app.reactHost.createSurface(context, "SimpleTestComponent", null) + val themedContext = ContextThemeWrapper(context, context.applicationInfo.theme) + val surface = app.reactHost.createSurface(themedContext, "SimpleTestComponent", null) val view = surface.view ?: throw IllegalStateException("ReactSurface returned a null view") @@ -62,14 +69,44 @@ class IsolatedTest { surface.start() } - // Wait for Fabric to commit its render tree - Thread.sleep(500) + // Wait for JS to load and Fabric to mount children (up to 30s for cold start) + val deadline = System.currentTimeMillis() + 30_000L + while (System.currentTimeMillis() < deadline) { + waitTwoFrames() + var childCount = 0 + instrumentation.runOnMainSync { childCount = view.childCount } + if (childCount > 0) break + } - Screenshot.snap(view).setName("simple_test_component").record() + instrumentation.runOnMainSync { + // Child views added by Fabric default to hardware acceleration; + // set software layer recursively so Screenshot.snap() can capture them. + setLayerTypeSoftwareRecursively(view) + Screenshot.snap(view).setName("simple_test_component").record() + } instrumentation.runOnMainSync { surface.stop() wm.removeView(view) } } + + private fun setLayerTypeSoftwareRecursively(view: View) { + view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + if (view is ViewGroup) { + for (i in 0 until view.childCount) { + setLayerTypeSoftwareRecursively(view.getChildAt(i)) + } + } + } + + private fun waitTwoFrames() { + repeat(2) { + val latch = CountDownLatch(1) + Handler(Looper.getMainLooper()).post { + Choreographer.getInstance().postFrameCallback { latch.countDown() } + } + latch.await() + } + } }