From 30fd8a21ba0792bb86369371e5a449a3ad11d6f3 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 9 Apr 2026 15:25:15 -0400 Subject: [PATCH 1/2] Investigate OnHierarchyChangeListener as readiness indicator for screenshot Uses ViewGroup.setOnHierarchyChangeListener on the surface root view to get a direct, non-polling callback the moment Fabric mounts its first child. Followed by a re-layout pass before snapping the screenshot. Produces a non-blank screenshot without polling. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/testapp/IsolatedTest.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index 59c03d0..f009416 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -1,5 +1,9 @@ package com.rnstorybookautoscreenshots +import android.content.Context +import android.graphics.Color +import android.view.View +import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.testapp.MainApplication @@ -9,8 +13,11 @@ import org.junit.Test import org.junit.runner.RunWith import com.facebook.testing.screenshot.ViewHelpers import com.facebook.testing.screenshot.Screenshot +import com.facebook.testing.screenshot.WindowAttachment import org.junit.Assert.*; import com.facebook.react.interfaces.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class IsolatedTest { @@ -46,6 +53,60 @@ class IsolatedTest { Screenshot.snap(surface.view!!) .record() } + + @Test + fun addViewHookTest() { + 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!! as android.view.ViewGroup + val latch = CountDownLatch(1) + var detacher: WindowAttachment.Detacher? = null + + try { + instrumentation.runOnMainSync { + view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + view.setBackgroundColor(Color.WHITE) + view.setOnHierarchyChangeListener(object : android.view.ViewGroup.OnHierarchyChangeListener { + override fun onChildViewAdded(parent: View, child: View) { + view.setOnHierarchyChangeListener(null) + latch.countDown() + } + override fun onChildViewRemoved(parent: View, child: View) {} + }) + detacher = WindowAttachment.dispatchAttach(view) + app.reactHost.onHostResume(null) + ViewHelpers.setupView(view).setExactWidthPx(1080).setExactHeightPx(1920).layout() + surface.start() + } + + assertTrue( + "Timed out waiting for Fabric to mount first child", + latch.await(30, TimeUnit.SECONDS) + ) + + instrumentation.runOnMainSync { + ViewHelpers.setupView(view).setExactWidthPx(1080).setExactHeightPx(1920).layout() + Screenshot.snap(view).setName("addViewHookTest").record() + } + } finally { + instrumentation.runOnMainSync { + surface.stop() + detacher?.detach() + } + } + } +} + +class FirstChildLatch(context: Context) : FrameLayout(context) { + val latch = CountDownLatch(1) + + override fun addView(child: View, index: Int, params: android.view.ViewGroup.LayoutParams) { + super.addView(child, index, params) + latch.countDown() + } } fun assertGoodTask(ti : TaskInterface) { From 4b6f67297a4b51a2236be9b98267614cb3506208 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 9 Apr 2026 15:31:16 -0400 Subject: [PATCH 2/2] Add hierarchyListenerTest to demonstrate OnHierarchyChangeListener indicator Adds a test that asserts the listener fires without taking a screenshot, mirroring the childCountTest structure on the predraw-listener branch. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/testapp/IsolatedTest.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index f009416..177e9cc 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -54,6 +54,46 @@ class IsolatedTest { .record() } + @Test + fun hierarchyListenerTest() { + 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!! as android.view.ViewGroup + val latch = CountDownLatch(1) + var detacher: WindowAttachment.Detacher? = null + + try { + instrumentation.runOnMainSync { + view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + view.setBackgroundColor(Color.WHITE) + view.setOnHierarchyChangeListener(object : android.view.ViewGroup.OnHierarchyChangeListener { + override fun onChildViewAdded(parent: View, child: View) { + view.setOnHierarchyChangeListener(null) + latch.countDown() + } + override fun onChildViewRemoved(parent: View, child: View) {} + }) + detacher = WindowAttachment.dispatchAttach(view) + app.reactHost.onHostResume(null) + ViewHelpers.setupView(view).setExactWidthPx(1080).setExactHeightPx(1920).layout() + surface.start() + } + + assertTrue( + "Timed out waiting for Fabric to mount first child", + latch.await(30, TimeUnit.SECONDS) + ) + } finally { + instrumentation.runOnMainSync { + surface.stop() + detacher?.detach() + } + } + } + @Test fun addViewHookTest() { val instrumentation = InstrumentationRegistry.getInstrumentation()