From f68d751a46104bbeab276ad5ba4c948ce23d4183 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 3 Apr 2026 14:37:14 -0400 Subject: [PATCH 1/2] Explore bridgeless constructViewTest with WindowManager + CONTENT_APPEARED Two key findings: - reactHost.onHostResume(null) must be called before surface.start() or Fabric won't commit mutations even though JS renders successfully - TYPE_APPLICATION_OVERLAY gives Fabric a real Window so the Choreographer fires and commits the shadow tree; ViewHelpers.setupView is not enough Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/testapp/IsolatedTest.kt | 109 +++++++++++++++--- 1 file changed, 90 insertions(+), 19 deletions(-) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index 59c03d0..ca210ea 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -1,53 +1,124 @@ package com.rnstorybookautoscreenshots +import android.Manifest +import android.app.Application +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 com.testapp.MainApplication +import androidx.test.rule.GrantPermissionRule +import com.facebook.react.PackageList +import com.facebook.react.bridge.ReactMarker +import com.facebook.react.bridge.ReactMarkerConstants +import com.facebook.react.common.annotations.UnstableReactNativeAPI +import com.facebook.react.bridge.JSBundleLoader +import com.facebook.react.defaults.DefaultComponentsRegistry +import com.facebook.react.defaults.DefaultReactHostDelegate +import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate +import com.facebook.react.fabric.ComponentFactory +import com.facebook.react.runtime.ReactHostImpl +import com.facebook.react.runtime.hermes.HermesInstance import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import com.facebook.testing.screenshot.ViewHelpers import com.facebook.testing.screenshot.Screenshot import org.junit.Assert.*; import com.facebook.react.interfaces.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class IsolatedTest { + + @get:Rule + val overlayPermission: GrantPermissionRule = + GrantPermissionRule.grant(Manifest.permission.SYSTEM_ALERT_WINDOW) + @Test fun simpleTest() { assertTrue(true) } + @OptIn(UnstableReactNativeAPI::class) @Test fun constructViewTest() { - val context = InstrumentationRegistry.getInstrumentation().targetContext - val app = context.applicationContext as MainApplication - val surface = app.reactHost.createSurface(context, "SimpleTestComponent", null) - assertEquals("SimpleTestComponent", surface.moduleName) - - // TODO: we aren't 100% sure if prerender() and start() are being called the way we want it to. - // We probably want to create a ReactHost directly instead of taking it from the MainApplication... probably - // Also look up bridge-less mode. - assertGoodTask(surface.prerender()) + val instrumentation = InstrumentationRegistry.getInstrumentation() + val context = instrumentation.targetContext + val reactHost = createTestReactHost(context) + val renderLatch = CountDownLatch(1) + val markerListener = ReactMarker.MarkerListener { name, _, _ -> + if (name == ReactMarkerConstants.CONTENT_APPEARED) renderLatch.countDown() + } + ReactMarker.addListener(markerListener) + val surface = reactHost.createSurface(context, "SimpleTestComponent", null) + assertEquals("SimpleTestComponent", surface.moduleName) assertNotNull(surface.view) - ViewHelpers.setupView(surface.view!!) - .setExactHeightPx(1000) - .setExactWidthPx(1000) - .layout() + val view = surface.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=0 so the compositor skips drawing while Fabric still sees a real Window. + alpha = 0f + } + try { + instrumentation.runOnMainSync { + // RESUMED state is required for Fabric to commit mutations to the view. + reactHost.onHostResume(null) + // Software rendering so Screenshot.snap() can capture via draw(canvas). + view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + wm.addView(view, params) + surface.start() + } - val ti = surface.start() - assertGoodTask(ti) + assertTrue("Timed out waiting for first render", renderLatch.await(10, TimeUnit.SECONDS)) - Screenshot.snap(surface.view!!) - .record() + Screenshot.snap(view).record() + } finally { + ReactMarker.removeListener(markerListener) + instrumentation.runOnMainSync { + surface.stop() + wm.removeView(view) + } + } } } +@OptIn(UnstableReactNativeAPI::class) +private fun createTestReactHost(context: android.content.Context): ReactHostImpl { + val app = context.applicationContext as Application + val bundleLoader = JSBundleLoader.createAssetLoader( + context, "assets://index.android.bundle", true) + val delegate = DefaultReactHostDelegate( + jsMainModulePath = "index", + jsBundleLoader = bundleLoader, + reactPackages = PackageList(app).packages, + jsRuntimeFactory = HermesInstance(), + turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(), + exceptionHandler = { throw it }, + ) + val componentFactory = ComponentFactory() + DefaultComponentsRegistry.register(componentFactory) + return ReactHostImpl( + context, + delegate, + componentFactory, + false, // allowPackagerServerAccess + false, // useDevSupport + ) +} + fun assertGoodTask(ti : TaskInterface) { ti.waitForCompletion() assertFalse(ti.isFaulted()) From f0ab3daf665fd105d30937835b18c2b8a8b35f6a Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 3 Apr 2026 15:19:57 -0400 Subject: [PATCH 2/2] Set white background on view before taking screenshot Co-Authored-By: Claude Sonnet 4.6 --- android/app/src/androidTest/java/com/testapp/IsolatedTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index ca210ea..42d71c1 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -2,6 +2,7 @@ package com.rnstorybookautoscreenshots import android.Manifest import android.app.Application +import android.graphics.Color import android.graphics.PixelFormat import android.view.View import android.view.WindowManager @@ -78,6 +79,7 @@ class IsolatedTest { reactHost.onHostResume(null) // Software rendering so Screenshot.snap() can capture via draw(canvas). view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + view.setBackgroundColor(Color.WHITE) wm.addView(view, params) surface.start() }