Skip to content

Commit

Permalink
Shortcut emitDeviceEvent in bridgeless (2nd attempt) (#44590)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #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
  • Loading branch information
javache authored and facebook-github-bot committed May 23, 2024
1 parent 1343313 commit b2ced62
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
&& mInteropModuleRegistry.shouldReturnInteropModule(jsInterface)) {
return mInteropModuleRegistry.getInteropModule(jsInterface);
}

// TODO T189052462: ReactContext caches JavaScriptModule instances
JavaScriptModule interfaceProxy =
(JavaScriptModule)
Proxy.newProxyInstance(
Expand All @@ -157,6 +159,13 @@ public <T extends JavaScriptModule> T getJSModule(Class<T> jsInterface) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit b2ced62

Please sign in to comment.