From b2ced62d3363b9448f6e4698692c31ed2d9230aa Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Thu, 23 May 2024 14:57:00 -0700 Subject: [PATCH] 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 --- .../react/runtime/BridgelessReactContext.java | 9 +++++ .../runtime/BridgelessReactContextTest.kt | 39 ++++++++++++++----- .../testutils/shadows/ShadowArguments.kt | 14 +++++++ .../testutils/shadows/ShadowNativeArray.kt | 29 ++++++++++++++ 4 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/testutils/shadows/ShadowNativeArray.kt 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 + } +}