Skip to content
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Investigate OnHierarchyChangeListener as readiness indicator for scre…
…enshot

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 <noreply@anthropic.com>
  • Loading branch information
EmilioBejasa and claude committed Apr 9, 2026
commit 30fd8a21ba0792bb86369371e5a449a3ad11d6f3
61 changes: 61 additions & 0 deletions android/app/src/androidTest/java/com/testapp/IsolatedTest.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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<Void>) {
Expand Down