diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.java index 6c7b343d25b..78f4e0c59a8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/BridgelessReactContext.java @@ -148,6 +148,8 @@ class BridgelessReactContext extends ReactApplicationContext implements EventDis && mInteropModuleRegistry.shouldReturnInteropModule(jsInterface)) { return mInteropModuleRegistry.getInteropModule(jsInterface); } + + // TODO T189052462: ReactContext caches JavaScriptModule instances JavaScriptModule interfaceProxy = (JavaScriptModule) Proxy.newProxyInstance( @@ -157,6 +159,13 @@ class BridgelessReactContext extends ReactApplicationContext implements EventDis return (T) interfaceProxy; } + /** Shortcut RCTDeviceEventEmitter.emit since it's frequently used */ + @Override + public void emitDeviceEvent(String eventName, @Nullable Object args) { + mReactHost.callFunctionOnModule( + "RCTDeviceEventEmitter", "emit", Arguments.fromJavaArgs(new Object[] {eventName, args})); + } + @Override public boolean hasNativeModule(Class nativeModuleInterface) { return mReactHost.hasNativeModule(nativeModuleInterface); diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/BridgelessReactContextTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/BridgelessReactContextTest.kt index a3bf2053394..e2703be942a 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/BridgelessReactContextTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/runtime/BridgelessReactContextTest.kt @@ -9,23 +9,31 @@ package com.facebook.react.runtime import android.app.Activity import android.content.Context +import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.fabric.FabricUIManager import com.facebook.react.uimanager.UIManagerModule +import com.facebook.testutils.shadows.ShadowArguments +import com.facebook.testutils.shadows.ShadowNativeArray import com.facebook.testutils.shadows.ShadowSoLoader -import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers -import org.mockito.Mockito import org.mockito.Mockito.doReturn +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config /** Tests [BridgelessReactContext] */ @RunWith(RobolectricTestRunner::class) -@Config(shadows = [ShadowSoLoader::class]) +@Config( + shadows = [ShadowSoLoader::class, ShadowArguments::class, ShadowNativeArray.Writable::class]) class BridgelessReactContextTest { private lateinit var context: Context private lateinit var reactHost: ReactHostImpl @@ -34,30 +42,43 @@ class BridgelessReactContextTest { @Before fun setUp() { context = Robolectric.buildActivity(Activity::class.java).create().get() - reactHost = Mockito.mock(ReactHostImpl::class.java) + reactHost = mock(ReactHostImpl::class.java) bridgelessReactContext = BridgelessReactContext(context, reactHost) } @Test fun getNativeModuleTest() { - val mUiManagerModule = Mockito.mock(UIManagerModule::class.java) + val mUiManagerModule = mock(UIManagerModule::class.java) doReturn(mUiManagerModule) .`when`(reactHost) .getNativeModule(ArgumentMatchers.any>()) val uiManagerModule = bridgelessReactContext.getNativeModule(UIManagerModule::class.java) - Assertions.assertThat(uiManagerModule).isEqualTo(mUiManagerModule) + assertThat(uiManagerModule).isEqualTo(mUiManagerModule) } @Test fun getFabricUIManagerTest() { - val fabricUiManager = Mockito.mock(FabricUIManager::class.java) + val fabricUiManager = mock(FabricUIManager::class.java) doReturn(fabricUiManager).`when`(reactHost).uiManager - Assertions.assertThat(bridgelessReactContext.getFabricUIManager()).isEqualTo(fabricUiManager) + assertThat(bridgelessReactContext.getFabricUIManager()).isEqualTo(fabricUiManager) } @Test fun getCatalystInstanceTest() { - Assertions.assertThat(bridgelessReactContext.getCatalystInstance()) + assertThat(bridgelessReactContext.getCatalystInstance()) .isInstanceOf(BridgelessCatalystInstance::class.java) } + + @Test + fun testEmitDeviceEvent() { + bridgelessReactContext.emitDeviceEvent("onNetworkResponseReceived", mapOf("foo" to "bar")) + + val argsCapture = ArgumentCaptor.forClass(WritableNativeArray::class.java) + verify(reactHost, times(1)) + .callFunctionOnModule(eq("RCTDeviceEventEmitter"), eq("emit"), argsCapture.capture()) + + val argsList = ShadowNativeArray.getContents(argsCapture.value) + assertThat(argsList[0]).isEqualTo("onNetworkResponseReceived") + @Suppress("UNCHECKED_CAST") assertThat((argsList[1] as Map)["foo"]).isEqualTo("bar") + } } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowArguments.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowArguments.kt index c8e47d67809..7307d503cd0 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowArguments.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowArguments.kt @@ -8,14 +8,28 @@ package com.facebook.testutils.shadows import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeArray import org.robolectric.annotation.Implementation import org.robolectric.annotation.Implements +import org.robolectric.shadow.api.Shadow @Implements(Arguments::class) class ShadowArguments { + companion object { + @JvmStatic @Implementation fun createArray(): WritableArray = JavaOnlyArray() + @JvmStatic @Implementation fun createMap(): WritableMap = JavaOnlyMap() + + @JvmStatic + @Implementation + fun fromJavaArgs(args: Array): WritableNativeArray = + WritableNativeArray().apply { + (Shadow.extract(this) as ShadowNativeArray).contents = args.toList() + } } } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowNativeArray.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowNativeArray.kt new file mode 100644 index 00000000000..f77a3a33def --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowNativeArray.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.testutils.shadows + +import com.facebook.react.bridge.NativeArray +import com.facebook.react.bridge.ReadableNativeArray +import com.facebook.react.bridge.WritableNativeArray +import org.robolectric.annotation.Implements +import org.robolectric.shadow.api.Shadow + +// Mockito can't mock native methods, so shadow the entire class instead +@Implements(NativeArray::class) +public open class ShadowNativeArray { + public var contents: List = mutableListOf() + + @Implements(ReadableNativeArray::class) public class Readable : ShadowNativeArray() {} + + @Implements(WritableNativeArray::class) public class Writable : ShadowNativeArray() {} + + public companion object { + public fun getContents(array: NativeArray): List = + (Shadow.extract(array) as ShadowNativeArray).contents + } +}