Shortcut emitDeviceEvent in bridgeless (2nd attempt) (#44590)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/44590

emitDeviceEvent is frequently used for perf-critical operations such as sending network responses from native to JS. We don't need to go through JavaScriptModule Proxy (which is missing caching in bridgeless) and instead can immediately invoke the callable JS module.

Changelog: [Internal]

Reviewed By: philIip

Differential Revision: D57435750

fbshipit-source-id: 1c120073ac80afd95deb8e3e6f1c00c2d3d80133
This commit is contained in:
Pieter De Baets
2024-05-23 14:57:00 -07:00
committed by Facebook GitHub Bot
parent 1343313dc6
commit b2ced62d33
4 changed files with 82 additions and 9 deletions
@@ -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 <T extends NativeModule> boolean hasNativeModule(Class<T> nativeModuleInterface) {
return mReactHost.hasNativeModule(nativeModuleInterface);
@@ -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<Class<UIManagerModule>>())
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<Any, Any>)["foo"]).isEqualTo("bar")
}
}
@@ -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<Any?>): WritableNativeArray =
WritableNativeArray().apply {
(Shadow.extract(this) as ShadowNativeArray).contents = args.toList()
}
}
}
@@ -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<Any?> = mutableListOf()
@Implements(ReadableNativeArray::class) public class Readable : ShadowNativeArray() {}
@Implements(WritableNativeArray::class) public class Writable : ShadowNativeArray() {}
public companion object {
public fun getContents(array: NativeArray): List<Any?> =
(Shadow.extract(array) as ShadowNativeArray).contents
}
}