From 455cdb1f8eab91585326da622b25f11b60dde1e8 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 13 Apr 2026 17:52:33 -0400 Subject: [PATCH 1/6] Replace ViewHelpers with explicit measure/layout; replace poll loop with view.post check - Remove ViewHelpers from constructViewTest and childCountTest (IsolatedTest.kt) and testSimpleTextView (ScreenshotTest.kt); use explicit measure()/layout() calls - Replace 30s poll loop in childCountTest with a self-rescheduling Runnable posted via view.post(), driven by the main-thread Handler that WindowAttachment.dispatchAttach installs in AttachInfo; block test thread on CompletableFuture.get(5s) - Remove childCountScreenshotTest (duplicate of childCountTest + screenshot) - assertGoodTask now captures and awaits startTask so the bg executor is drained before the future is polled Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/testapp/IsolatedTest.kt | 99 +++++++------------ .../java/com/testapp/ScreenshotTest.kt | 12 +-- 2 files changed, 41 insertions(+), 70 deletions(-) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index 8ac5384..0d9da6b 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -1,7 +1,7 @@ package com.rnstorybookautoscreenshots -import android.graphics.Color import android.view.View +import android.view.ViewGroup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.testapp.MainApplication @@ -9,11 +9,12 @@ import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue 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 org.junit.Assert.* import com.facebook.react.interfaces.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class IsolatedTest { @@ -29,19 +30,16 @@ class IsolatedTest { 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()) - assertNotNull(surface.view) - ViewHelpers.setupView(surface.view!!) - .setExactHeightPx(1000) - .setExactWidthPx(1000) - .layout() - + val view = surface.view!! + view.measure( + View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY) + ) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) val ti = surface.start() assertGoodTask(ti) @@ -57,66 +55,39 @@ class IsolatedTest { val app = context.applicationContext as MainApplication val surface = app.reactHost.createSurface(context, "SimpleTestComponent", null) - val view = surface.view!! + val view = surface.view!! as ViewGroup var detacher: WindowAttachment.Detacher? = null + var startTask: TaskInterface? = null + val childrenMounted = CompletableFuture() try { instrumentation.runOnMainSync { view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) - view.setBackgroundColor(Color.WHITE) detacher = WindowAttachment.dispatchAttach(view) app.reactHost.onHostResume(null) - ViewHelpers.setupView(view).setExactWidthPx(1080).setExactHeightPx(1920).layout() - surface.start() + view.measure( + View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY) + ) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) + startTask = surface.start() + // dispatchAttach gives the view a real Handler on the main Looper via AttachInfo, + // so view.post() routes to the main MessageQueue. Poll here (on the main thread, + // between Choreographer frames) until Fabric mounts the first child. + val check = object : Runnable { + override fun run() { + if (view.childCount > 0) childrenMounted.complete(view.childCount) + else view.postDelayed(this, 50) + } + } + view.post(check) } - val deadline = System.currentTimeMillis() + 30_000 - var hasChildren = false - while (!hasChildren && System.currentTimeMillis() < deadline) { - Thread.sleep(50) - instrumentation.runOnMainSync { hasChildren = view.childCount > 0 } - } - assertTrue("Timed out waiting for children", hasChildren) - } finally { - instrumentation.runOnMainSync { - surface.stop() - detacher?.detach() - } - } - } - - @Test - fun childCountScreenshotTest() { - 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!! - var detacher: WindowAttachment.Detacher? = null - - try { - instrumentation.runOnMainSync { - view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) - view.setBackgroundColor(Color.WHITE) - detacher = WindowAttachment.dispatchAttach(view) - app.reactHost.onHostResume(null) - ViewHelpers.setupView(view).setExactWidthPx(1080).setExactHeightPx(1920).layout() - surface.start() - } - - val deadline = System.currentTimeMillis() + 30_000 - var hasChildren = false - while (!hasChildren && System.currentTimeMillis() < deadline) { - Thread.sleep(50) - instrumentation.runOnMainSync { hasChildren = view.childCount > 0 } - } - assertTrue("Timed out waiting for children", hasChildren) - - instrumentation.runOnMainSync { - ViewHelpers.setupView(view).setExactWidthPx(1080).setExactHeightPx(1920).layout() - Screenshot.snap(view).setName("childCountReadyTest").record() - } + // start() fires on the bg executor. assertGoodTask blocks the test thread until + // it completes, keeping the main thread free for Choreographer and our check loop. + assertGoodTask(startTask!!) + val count = childrenMounted.get(5, TimeUnit.SECONDS) + assertTrue("Expected childCount > 0, but was $count", count > 0) } finally { instrumentation.runOnMainSync { surface.stop() @@ -126,7 +97,7 @@ class IsolatedTest { } } -fun assertGoodTask(ti : TaskInterface) { +fun assertGoodTask(ti: TaskInterface) { ti.waitForCompletion() assertFalse(ti.isFaulted()) assertTrue(ti.isCompleted()) diff --git a/android/app/src/androidTest/java/com/testapp/ScreenshotTest.kt b/android/app/src/androidTest/java/com/testapp/ScreenshotTest.kt index 1dffb5d..72e7754 100644 --- a/android/app/src/androidTest/java/com/testapp/ScreenshotTest.kt +++ b/android/app/src/androidTest/java/com/testapp/ScreenshotTest.kt @@ -10,7 +10,6 @@ import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.GrantPermissionRule import com.facebook.testing.screenshot.Screenshot -import com.facebook.testing.screenshot.ViewHelpers import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -53,11 +52,12 @@ class ScreenshotTest { view.setBackgroundColor(Color.WHITE) view.setPadding(20, 20, 20, 20) - // Measure and layout the view - ViewHelpers.setupView(view) - .setExactWidthDp(300) - .setExactHeightDp(100) - .layout() + val density = view.resources.displayMetrics.density + view.measure( + android.view.View.MeasureSpec.makeMeasureSpec((300 * density).toInt(), android.view.View.MeasureSpec.EXACTLY), + android.view.View.MeasureSpec.makeMeasureSpec((100 * density).toInt(), android.view.View.MeasureSpec.EXACTLY) + ) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) // Take screenshot Screenshot.snap(view) From f2ad8571db606a2e80da19adb4b17274c2d89df2 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 13 Apr 2026 17:58:14 -0400 Subject: [PATCH 2/6] Add childCountScreenshotTest: screenshot after waiting for children to mount Uses the same view.post polling mechanism as childCountTest to wait for Fabric to mount children before snapping the screenshot, ensuring the result is never blank. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/testapp/IsolatedTest.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index 0d9da6b..d1c9873 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -48,6 +48,50 @@ class IsolatedTest { .record() } + @Test + fun childCountScreenshotTest() { + 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 ViewGroup + var detacher: WindowAttachment.Detacher? = null + var startTask: TaskInterface? = null + val childrenMounted = CompletableFuture() + + try { + instrumentation.runOnMainSync { + view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + detacher = WindowAttachment.dispatchAttach(view) + app.reactHost.onHostResume(null) + view.measure( + View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY) + ) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) + startTask = surface.start() + val check = object : Runnable { + override fun run() { + if (view.childCount > 0) childrenMounted.complete(Unit) + else view.postDelayed(this, 50) + } + } + view.post(check) + } + + assertGoodTask(startTask!!) + childrenMounted.get(5, TimeUnit.SECONDS) + + Screenshot.snap(view).record() + } finally { + instrumentation.runOnMainSync { + surface.stop() + detacher?.detach() + } + } + } + @Test fun childCountTest() { val instrumentation = InstrumentationRegistry.getInstrumentation() From de22505b99eed64cae844b4fd5f6f72a4edf58f9 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 13 Apr 2026 18:16:16 -0400 Subject: [PATCH 3/6] ci: retrigger From 60607f507440b083fab15520819e6f0c6a7b1cc7 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Apr 2026 15:45:09 -0400 Subject: [PATCH 4/6] Simplify childCountScreenshotTest: remove concurrency and polling loop The previous version duplicated childCountTest's CompletableFuture coordination and view.postDelayed polling. Use prerender()+start() directly, matching the pattern of constructViewTest. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/testapp/IsolatedTest.kt | 44 +++++-------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index d1c9873..4a719eb 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -50,46 +50,22 @@ class IsolatedTest { @Test fun childCountScreenshotTest() { - val instrumentation = InstrumentationRegistry.getInstrumentation() - val context = instrumentation.targetContext + val context = InstrumentationRegistry.getInstrumentation().targetContext val app = context.applicationContext as MainApplication val surface = app.reactHost.createSurface(context, "SimpleTestComponent", null) - val view = surface.view!! as ViewGroup - var detacher: WindowAttachment.Detacher? = null - var startTask: TaskInterface? = null - val childrenMounted = CompletableFuture() + assertGoodTask(surface.prerender()) - try { - instrumentation.runOnMainSync { - view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) - detacher = WindowAttachment.dispatchAttach(view) - app.reactHost.onHostResume(null) - view.measure( - View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY) - ) - view.layout(0, 0, view.measuredWidth, view.measuredHeight) - startTask = surface.start() - val check = object : Runnable { - override fun run() { - if (view.childCount > 0) childrenMounted.complete(Unit) - else view.postDelayed(this, 50) - } - } - view.post(check) - } + val view = surface.view!! as ViewGroup + view.measure( + View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY) + ) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) - assertGoodTask(startTask!!) - childrenMounted.get(5, TimeUnit.SECONDS) + assertGoodTask(surface.start()) - Screenshot.snap(view).record() - } finally { - instrumentation.runOnMainSync { - surface.stop() - detacher?.detach() - } - } + Screenshot.snap(view).record() } @Test From 9ff8bd0532a5d5a26e425fc80c87ba4f0c4d1305 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Apr 2026 16:27:08 -0400 Subject: [PATCH 5/6] Add childCountSyncTest: assert childCount > 0 with no countdown latch One runOnMainSync for all setup + start(). waitUntil polls until children are mounted (exits immediately on success, 5s timeout), replacing the CompletableFuture + postDelayed loop from childCountTest. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/testapp/IsolatedTest.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index 4a719eb..9b5194d 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -68,6 +68,41 @@ class IsolatedTest { Screenshot.snap(view).record() } + @Test + fun childCountSyncTest() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + val context = instrumentation.targetContext + val app = context.applicationContext as MainApplication + val surface = app.reactHost.createSurface(context, "SimpleTestComponent", null) + + assertGoodTask(surface.prerender()) + + val view = surface.view!! as ViewGroup + var detacher: WindowAttachment.Detacher? = null + + var startTask: TaskInterface? = null + + try { + instrumentation.runOnMainSync { + view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + detacher = WindowAttachment.dispatchAttach(view) + app.reactHost.onHostResume(null) + view.measure( + View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY) + ) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) + startTask = surface.start() + } + + assertGoodTask(startTask!!) + waitUntil { view.childCount > 0 } + assertTrue("Expected childCount > 0, but was ${view.childCount}", view.childCount > 0) + } finally { + instrumentation.runOnMainSync { detacher?.detach() } + } + } + @Test fun childCountTest() { val instrumentation = InstrumentationRegistry.getInstrumentation() @@ -117,6 +152,14 @@ class IsolatedTest { } } +fun waitUntil(timeoutMs: Long = 5000, condition: () -> Boolean) { + val deadline = System.currentTimeMillis() + timeoutMs + while (!condition()) { + check(System.currentTimeMillis() < deadline) { "Condition not met within ${timeoutMs}ms" } + Thread.sleep(16) + } +} + fun assertGoodTask(ti: TaskInterface) { ti.waitForCompletion() assertFalse(ti.isFaulted()) From 60f68969e37855339bd4e08320f7433c269eb6b5 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Apr 2026 16:50:50 -0400 Subject: [PATCH 6/6] Fix childCountScreenshotTest: wait for children before snapping Use dispatchAttach + onHostResume + waitUntil so Fabric mounts children before the screenshot is taken. Previously the screenshot was blank. Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/testapp/IsolatedTest.kt | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt index 9b5194d..bc9a05a 100644 --- a/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt +++ b/android/app/src/androidTest/java/com/testapp/IsolatedTest.kt @@ -50,22 +50,37 @@ class IsolatedTest { @Test fun childCountScreenshotTest() { - val context = InstrumentationRegistry.getInstrumentation().targetContext + val instrumentation = InstrumentationRegistry.getInstrumentation() + val context = instrumentation.targetContext val app = context.applicationContext as MainApplication val surface = app.reactHost.createSurface(context, "SimpleTestComponent", null) assertGoodTask(surface.prerender()) val view = surface.view!! as ViewGroup - view.measure( - View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY) - ) - view.layout(0, 0, view.measuredWidth, view.measuredHeight) + var detacher: WindowAttachment.Detacher? = null + var startTask: TaskInterface? = null + + try { + instrumentation.runOnMainSync { + view.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + detacher = WindowAttachment.dispatchAttach(view) + app.reactHost.onHostResume(null) + view.measure( + View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY) + ) + view.layout(0, 0, view.measuredWidth, view.measuredHeight) + startTask = surface.start() + } - assertGoodTask(surface.start()) + assertGoodTask(startTask!!) + waitUntil { view.childCount > 0 } - Screenshot.snap(view).record() + Screenshot.snap(view).record() + } finally { + instrumentation.runOnMainSync { detacher?.detach() } + } } @Test