Skip to content
92 changes: 73 additions & 19 deletions android/app/src/androidTest/java/com/testapp/IsolatedTest.kt
Original file line number Diff line number Diff line change
@@ -1,50 +1,104 @@
package com.rnstorybookautoscreenshots

import android.app.Application
import android.graphics.Color
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.testapp.MainApplication
import com.facebook.react.PackageList
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.Test
import org.junit.runner.RunWith
import com.facebook.testing.screenshot.ViewHelpers
import com.facebook.testing.screenshot.Screenshot
import com.facebook.testing.screenshot.ViewHelpers
import com.facebook.testing.screenshot.WindowAttachment
import org.junit.Assert.*;
import com.facebook.react.interfaces.*

@RunWith(AndroidJUnit4::class)
class IsolatedTest {

@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 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)
val reactHost = ReactHostImpl(
context,
delegate,
componentFactory,
false, // allowPackagerServerAccess
false, // useDevSupport
)

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!!

var detacher: WindowAttachment.Detacher? = null
var startTask: com.facebook.react.interfaces.TaskInterface<Void>? = null

try {
instrumentation.runOnMainSync {
reactHost.onHostResume(null)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also interesting. I wonder if the onHostResume depends on the Window being attached for it to do anything. Is there a reason this is before dispatchAttach?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. We tested moving it after dispatchAttach — no behavioral difference. Looking at the source, onHostResume calls moveToOnHostResume(currentReactContext, activity) but currentReactContext is null at that point (surface hasn't started yet), so it's effectively a no-op either way. We've reordered it after dispatchAttach for correctness of intent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We

is this something you copy pasted from Claude, or did you actually test this?

so it's effectively a no-op either way

then why make the change? Adding extra code that does nothing makes it harder to figure out what's relevant and what's not. I don't care much for "correctness of intent"

view.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
view.setBackgroundColor(Color.WHITE)
detacher = WindowAttachment.dispatchAttach(view)
ViewHelpers.setupView(view).setExactWidthPx(1080).setExactHeightPx(1920).layout()
startTask = surface.start()
}

// Wait for the surface start task to complete off the main thread.
assertGoodTask(startTask!!)

val ti = surface.start()
assertGoodTask(ti)
// Poll until Fabric has mounted child views into the surface.
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 React Native to render content", hasChildren)

Screenshot.snap(surface.view!!)
.record()
instrumentation.runOnMainSync {
Screenshot.snap(view).setName("SimpleTestComponent").record()
}
} finally {
instrumentation.runOnMainSync {
surface.stop()
detacher?.detach()
}
}
}
}

Expand Down
Loading