diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java index b361bac736f31c..8765116263c9fb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java @@ -21,6 +21,7 @@ import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.ThreadConfined; import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.interop.InteropModuleRegistry; import com.facebook.react.bridge.queue.MessageQueueThread; import com.facebook.react.bridge.queue.ReactQueueConfiguration; import com.facebook.react.common.LifecycleState; @@ -69,6 +70,8 @@ public interface RCTDeviceEventEmitter extends JavaScriptModule { private @Nullable JSExceptionHandler mJSExceptionHandler; private @Nullable JSExceptionHandler mExceptionHandlerWrapper; private @Nullable WeakReference mCurrentActivity; + + private @Nullable InteropModuleRegistry mInteropModuleRegistry; private boolean mIsInitialized = false; public ReactContext(Context base) { @@ -93,6 +96,7 @@ public void initializeWithInstance(CatalystInstance catalystInstance) { ReactQueueConfiguration queueConfig = catalystInstance.getReactQueueConfiguration(); initializeMessageQueueThreads(queueConfig); + initializeInteropModules(); } /** Initialize message queue threads using a ReactQueueConfiguration. */ @@ -120,6 +124,14 @@ public synchronized void initializeMessageQueueThreads(ReactQueueConfiguration q mIsInitialized = true; } + protected void initializeInteropModules() { + mInteropModuleRegistry = new InteropModuleRegistry(); + } + + protected void initializeInteropModules(ReactContext reactContext) { + mInteropModuleRegistry = reactContext.mInteropModuleRegistry; + } + public void resetPerfStats() { if (mNativeModulesMessageQueueThread != null) { mNativeModulesMessageQueueThread.resetPerfStats(); @@ -163,6 +175,10 @@ public T getJSModule(Class jsInterface) { } throw new IllegalStateException(EARLY_JS_ACCESS_EXCEPTION_MESSAGE); } + if (mInteropModuleRegistry != null + && mInteropModuleRegistry.shouldReturnInteropModule(jsInterface)) { + return mInteropModuleRegistry.getInteropModule(jsInterface); + } return mCatalystInstance.getJSModule(jsInterface); } @@ -543,4 +559,17 @@ public void registerSegment(int segmentId, String path, Callback callback) { Assertions.assertNotNull(mCatalystInstance).registerSegment(segmentId, path); Assertions.assertNotNull(callback).invoke(); } + + /** + * Register a {@link JavaScriptModule} within the Interop Layer so that can be consumed whenever + * getJSModule is invoked. + * + *

This method is internal to React Native and should not be used externally. + */ + public void internal_registerInteropModule( + Class interopModuleInterface, Object interopModule) { + if (mInteropModuleRegistry != null) { + mInteropModuleRegistry.registerInteropModule(interopModuleInterface, interopModule); + } + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/interop/InteropModuleRegistry.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/interop/InteropModuleRegistry.java new file mode 100644 index 00000000000000..f47199edc63d80 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/interop/InteropModuleRegistry.java @@ -0,0 +1,56 @@ +/* + * 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.react.bridge.interop; + +import androidx.annotation.Nullable; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.config.ReactFeatureFlags; +import java.util.HashMap; + +/** + * A utility class that takes care of returning {@link JavaScriptModule} which are used for the + * Fabric Interop Layer. This allows us to override the returned classes once the user is invoking + * `ReactContext.getJsModule()`. + * + *

Currently we only support a `RCTEventEmitter` re-implementation, being `InteropEventEmitter` + * but this class can support other re-implementation in the future. + */ +public class InteropModuleRegistry { + + @SuppressWarnings("rawtypes") + private final HashMap supportedModules; + + public InteropModuleRegistry() { + this.supportedModules = new HashMap<>(); + } + + public boolean shouldReturnInteropModule(Class requestedModule) { + return checkReactFeatureFlagsConditions() && supportedModules.containsKey(requestedModule); + } + + @Nullable + public T getInteropModule(Class requestedModule) { + if (checkReactFeatureFlagsConditions()) { + //noinspection unchecked + return (T) supportedModules.get(requestedModule); + } else { + return null; + } + } + + public void registerInteropModule( + Class interopModuleInterface, Object interopModule) { + if (checkReactFeatureFlagsConditions()) { + supportedModules.put(interopModuleInterface, interopModule); + } + } + + private boolean checkReactFeatureFlagsConditions() { + return ReactFeatureFlags.enableFabricRenderer && ReactFeatureFlags.unstable_useFabricInterop; + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 9884ba9e687e28..fd479424f63787 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -42,6 +42,14 @@ public class ReactFeatureFlags { */ public static volatile boolean enableFabricRenderer = false; + /** + * Should this application enable the Fabric Interop Layer for Android? If yes, the application + * will behave so that it can accept non-Fabric components and render them on Fabric. This toggle + * is controlling extra logic such as custom event dispatching that are needed for the Fabric + * Interop Layer to work correctly. + */ + public static volatile boolean unstable_useFabricInterop = false; + /** * Feature flag to enable the new bridgeless architecture. Note: Enabling this will force enable * the following flags: `useTurboModules` & `enableFabricRenderer`. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt index 4b5b71e4d9c03a..73cb1e0f1da939 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/defaults/DefaultNewArchitectureEntryPoint.kt @@ -31,6 +31,7 @@ object DefaultNewArchitectureEntryPoint { ) { ReactFeatureFlags.useTurboModules = turboModulesEnabled ReactFeatureFlags.enableFabricRenderer = fabricEnabled + ReactFeatureFlags.unstable_useFabricInterop = fabricEnabled this.privateFabricEnabled = fabricEnabled this.privateTurboModulesEnabled = turboModulesEnabled diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 445180c71bd2ee..f4e9e6e308e5a2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -55,6 +55,7 @@ import com.facebook.react.fabric.events.EventBeatManager; import com.facebook.react.fabric.events.EventEmitterWrapper; import com.facebook.react.fabric.events.FabricEventEmitter; +import com.facebook.react.fabric.interop.InteropEventEmitter; import com.facebook.react.fabric.mounting.MountItemDispatcher; import com.facebook.react.fabric.mounting.MountingManager; import com.facebook.react.fabric.mounting.SurfaceMountingManager; @@ -77,6 +78,7 @@ import com.facebook.react.uimanager.events.EventCategoryDef; import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.uimanager.events.EventDispatcherImpl; +import com.facebook.react.uimanager.events.RCTEventEmitter; import com.facebook.react.views.text.TextLayoutManager; import com.facebook.react.views.text.TextLayoutManagerMapBuffer; import java.util.HashMap; @@ -383,6 +385,11 @@ public void initialize() { ReactMarker.addFabricListener(mDevToolsReactPerfLogger); } + if (ReactFeatureFlags.unstable_useFabricInterop) { + InteropEventEmitter interopEventEmitter = new InteropEventEmitter(mReactApplicationContext); + mReactApplicationContext.internal_registerInteropModule( + RCTEventEmitter.class, interopEventEmitter); + } } // This is called on the JS thread (see CatalystInstanceImpl). diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/interop/InteropEvent.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/interop/InteropEvent.java new file mode 100644 index 00000000000000..881edbdf6fa4e1 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/interop/InteropEvent.java @@ -0,0 +1,41 @@ +/* + * 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.react.fabric.interop; + +import androidx.annotation.Nullable; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.uimanager.events.Event; + +/** + * An {@link Event} class used by the {@link InteropEventEmitter}. This class is just holding the + * event name and the data which is received by the `receiveEvent` method and will be passed over + * the the {@link com.facebook.react.uimanager.events.EventDispatcher} + */ +class InteropEvent extends Event { + + private final String mName; + private final WritableMap mEventData; + + InteropEvent(String name, @Nullable WritableMap eventData, int surfaceId, int viewTag) { + super(surfaceId, viewTag); + mName = name; + mEventData = eventData; + } + + @Override + public String getEventName() { + return mName; + } + + @Override + @VisibleForTesting + public WritableMap getEventData() { + return mEventData; + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/interop/InteropEventEmitter.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/interop/InteropEventEmitter.java new file mode 100644 index 00000000000000..aef10bffdfbdcf --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/interop/InteropEventEmitter.java @@ -0,0 +1,65 @@ +/* + * 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.react.fabric.interop; + +import androidx.annotation.Nullable; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * A reimplementation of {@link RCTEventEmitter} which is using a {@link EventDispatcher} under the + * hood. + * + *

On Fabric, you're supposed to use {@link EventDispatcher} to dispatch events. However, we + * provide an interop layer for non-Fabric migrated components. + * + *

This instance will be returned if the user is invoking `context.getJsModule(RCTEventEmitter) + * and is providing support for the `receiveEvent` method, so that non-Fabric ViewManagers can + * continue to deliver events also when Fabric is turned on. + */ +public class InteropEventEmitter implements RCTEventEmitter { + + private final ReactContext mReactContext; + + private @Nullable EventDispatcher mEventDispatcherOverride; + + public InteropEventEmitter(ReactContext reactContext) { + mReactContext = reactContext; + } + + @Override + public void receiveEvent(int targetReactTag, String eventName, @Nullable WritableMap eventData) { + EventDispatcher dispatcher; + if (mEventDispatcherOverride != null) { + dispatcher = mEventDispatcherOverride; + } else { + dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mReactContext, targetReactTag); + } + int surfaceId = UIManagerHelper.getSurfaceId(mReactContext); + if (dispatcher != null) { + dispatcher.dispatchEvent(new InteropEvent(eventName, eventData, surfaceId, targetReactTag)); + } + } + + @Override + public void receiveTouches( + String eventName, WritableArray touches, WritableArray changedIndices) { + throw new UnsupportedOperationException( + "EventEmitter#receiveTouches is not supported by the Fabric Interop Layer"); + } + + @VisibleForTesting + void overrideEventDispatcher(EventDispatcher eventDispatcherOverride) { + mEventDispatcherOverride = eventDispatcherOverride; + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java index 28cd1855473d35..b10d2d9b3c0681 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ThemedReactContext.java @@ -50,6 +50,7 @@ public ThemedReactContext( if (reactApplicationContext.hasCatalystInstance()) { initializeWithInstance(reactApplicationContext.getCatalystInstance()); } + initializeInteropModules(reactApplicationContext); mReactApplicationContext = reactApplicationContext; mModuleName = moduleName; mSurfaceId = surfaceId; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java index a4d8facc08d9ec..a570c7acdf007d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java @@ -10,11 +10,15 @@ import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.facebook.react.common.MapBuilder; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.systrace.SystraceMessage; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Helps generate constants map for {@link UIManagerModule} by collecting and merging constants from @@ -113,6 +117,13 @@ Map viewManagerBubblingEvents = viewManager.getExportedCustomBubblingEventTypeConstants(); if (viewManagerBubblingEvents != null) { + + if (ReactFeatureFlags.enableFabricRenderer && ReactFeatureFlags.unstable_useFabricInterop) { + // For Fabric, events needs to be fired with a "top" prefix. + // For the sake of Fabric Interop, here we normalize events adding "top" in their + // name if the user hasn't provided it. + normalizeEventTypes(viewManagerBubblingEvents); + } recursiveMerge(cumulativeBubblingEventTypes, viewManagerBubblingEvents); recursiveMerge(viewManagerBubblingEvents, defaultBubblingEvents); viewManagerConstants.put(BUBBLING_EVENTS_KEY, viewManagerBubblingEvents); @@ -145,6 +156,27 @@ return viewManagerConstants; } + @VisibleForTesting + /* package */ static void normalizeEventTypes(Map events) { + if (events == null) { + return; + } + Set keysToNormalize = new HashSet<>(); + for (Object key : events.keySet()) { + if (key instanceof String) { + String keyString = (String) key; + if (!keyString.startsWith("top")) { + keysToNormalize.add(keyString); + } + } + } + for (String oldKey : keysToNormalize) { + Object value = events.get(oldKey); + String newKey = "top" + oldKey.substring(0, 1).toUpperCase() + oldKey.substring(1); + events.put(newKey, value); + } + } + /** Merges {@param source} map into {@param dest} map recursively */ private static void recursiveMerge(@Nullable Map dest, @Nullable Map source) { if (dest == null || source == null || source.isEmpty()) { diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/FakeRCTEventEmitter.java b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/FakeRCTEventEmitter.java new file mode 100644 index 00000000000000..8a2dc19f869c3c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/FakeRCTEventEmitter.java @@ -0,0 +1,23 @@ +/* + * 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.react.bridge.interop; + +import androidx.annotation.Nullable; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +public class FakeRCTEventEmitter implements RCTEventEmitter { + + @Override + public void receiveEvent(int targetReactTag, String eventName, @Nullable WritableMap event) {} + + @Override + public void receiveTouches( + String eventName, WritableArray touches, WritableArray changedIndices) {} +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.java b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.java new file mode 100644 index 00000000000000..158598078480d2 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/bridge/interop/InteropModuleRegistryTest.java @@ -0,0 +1,92 @@ +/* + * 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.react.bridge.interop; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.react.modules.core.JSTimers; +import com.facebook.react.uimanager.events.RCTEventEmitter; +import org.junit.Before; +import org.junit.Test; + +public class InteropModuleRegistryTest { + + InteropModuleRegistry underTest; + + @Before + public void setup() { + underTest = new InteropModuleRegistry(); + } + + @Test + public void shouldReturnInteropModule_withFabricDisabled_returnsFalse() { + ReactFeatureFlags.enableFabricRenderer = false; + + assertFalse(underTest.shouldReturnInteropModule(RCTEventEmitter.class)); + } + + @Test + public void shouldReturnInteropModule_withFabricInteropDisabled_returnsFalse() { + ReactFeatureFlags.enableFabricRenderer = true; + ReactFeatureFlags.unstable_useFabricInterop = false; + + assertFalse(underTest.shouldReturnInteropModule(RCTEventEmitter.class)); + } + + @Test + public void shouldReturnInteropModule_withUnregisteredClass_returnsFalse() { + ReactFeatureFlags.enableFabricRenderer = true; + ReactFeatureFlags.unstable_useFabricInterop = true; + + assertFalse(underTest.shouldReturnInteropModule(JSTimers.class)); + } + + @Test + public void shouldReturnInteropModule_withRegisteredClass_returnsTrue() { + ReactFeatureFlags.enableFabricRenderer = true; + ReactFeatureFlags.unstable_useFabricInterop = true; + + underTest.registerInteropModule(RCTEventEmitter.class, new FakeRCTEventEmitter()); + + assertTrue(underTest.shouldReturnInteropModule(RCTEventEmitter.class)); + } + + @Test + public void getInteropModule_withRegisteredClassAndInvalidFlags_returnsNull() { + ReactFeatureFlags.enableFabricRenderer = false; + ReactFeatureFlags.unstable_useFabricInterop = false; + underTest.registerInteropModule(RCTEventEmitter.class, new FakeRCTEventEmitter()); + + RCTEventEmitter interopModule = underTest.getInteropModule(RCTEventEmitter.class); + + assertNull(interopModule); + } + + @Test + public void getInteropModule_withRegisteredClassAndValidFlags_returnsInteropModule() { + ReactFeatureFlags.enableFabricRenderer = true; + ReactFeatureFlags.unstable_useFabricInterop = true; + underTest.registerInteropModule(RCTEventEmitter.class, new FakeRCTEventEmitter()); + + RCTEventEmitter interopModule = underTest.getInteropModule(RCTEventEmitter.class); + + assertTrue(interopModule instanceof FakeRCTEventEmitter); + } + + @Test + public void getInteropModule_withUnregisteredClass_returnsNull() { + ReactFeatureFlags.enableFabricRenderer = true; + ReactFeatureFlags.unstable_useFabricInterop = true; + JSTimers missingModule = underTest.getInteropModule(JSTimers.class); + + assertNull(missingModule); + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/interop/FakeEventDispatcher.java b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/interop/FakeEventDispatcher.java new file mode 100644 index 00000000000000..f747845b1533e4 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/interop/FakeEventDispatcher.java @@ -0,0 +1,55 @@ +/* + * 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.react.fabric.interop; + +import com.facebook.react.uimanager.events.BatchEventDispatchedListener; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.uimanager.events.EventDispatcherListener; +import com.facebook.react.uimanager.events.RCTEventEmitter; +import com.facebook.react.uimanager.events.RCTModernEventEmitter; +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("rawtypes") +public class FakeEventDispatcher implements EventDispatcher { + + List recordedDispatchedEvents = new ArrayList<>(); + + @Override + public void dispatchEvent(Event event) { + recordedDispatchedEvents.add(event); + } + + @Override + public void dispatchAllEvents() {} + + @Override + public void addListener(EventDispatcherListener listener) {} + + @Override + public void removeListener(EventDispatcherListener listener) {} + + @Override + public void addBatchEventDispatchedListener(BatchEventDispatchedListener listener) {} + + @Override + public void removeBatchEventDispatchedListener(BatchEventDispatchedListener listener) {} + + @Override + public void registerEventEmitter(int uiManagerType, RCTEventEmitter eventEmitter) {} + + @Override + public void registerEventEmitter(int uiManagerType, RCTModernEventEmitter eventEmitter) {} + + @Override + public void unregisterEventEmitter(int uiManagerType) {} + + @Override + public void onCatalystInstanceDestroyed() {} +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/interop/InteropEventEmitterTest.java b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/interop/InteropEventEmitterTest.java new file mode 100644 index 00000000000000..c26e9b36ac13fd --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/interop/InteropEventEmitterTest.java @@ -0,0 +1,66 @@ +/* + * 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.react.fabric.interop; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class InteropEventEmitterTest { + + ReactContext mReactContext; + FakeEventDispatcher mEventDispatcher; + + @Before + public void setup() { + mReactContext = new ReactApplicationContext(RuntimeEnvironment.application); + mEventDispatcher = new FakeEventDispatcher(); + } + + @Test + public void receiveEvent_dispatchesCorrectly() { + InteropEventEmitter eventEmitter = new InteropEventEmitter(mReactContext); + eventEmitter.overrideEventDispatcher(mEventDispatcher); + + eventEmitter.receiveEvent(42, "onTest", null); + + assertEquals(1, mEventDispatcher.recordedDispatchedEvents.size()); + assertEquals("onTest", mEventDispatcher.recordedDispatchedEvents.get(0).getEventName()); + assertEquals(InteropEvent.class, mEventDispatcher.recordedDispatchedEvents.get(0).getClass()); + } + + @Test + public void receiveEvent_dataIsPreserved() { + InteropEventEmitter eventEmitter = new InteropEventEmitter(mReactContext); + eventEmitter.overrideEventDispatcher(mEventDispatcher); + WritableMap eventData = JavaOnlyMap.of("color", "indigo"); + + eventEmitter.receiveEvent(42, "onTest", eventData); + + InteropEvent event = (InteropEvent) mEventDispatcher.recordedDispatchedEvents.get(0); + WritableMap dispatchedEventData = event.getEventData(); + assertNotNull(dispatchedEventData); + assertEquals("indigo", dispatchedEventData.getString("color")); + } + + @Test(expected = UnsupportedOperationException.class) + public void receiveTouches_isNotSupported() { + InteropEventEmitter eventEmitter = new InteropEventEmitter(mReactContext); + eventEmitter.receiveTouches("a touch", null, null); + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.java b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.java new file mode 100644 index 00000000000000..78ba487e519c91 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelperTest.java @@ -0,0 +1,89 @@ +/* + * 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.react.uimanager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.facebook.react.common.MapBuilder; +import java.util.Map; +import org.junit.Test; + +public class UIManagerModuleConstantsHelperTest { + + @Test + public void normalizeEventTypes_withNull_doesNothing() { + UIManagerModuleConstantsHelper.normalizeEventTypes(null); + } + + @Test + public void normalizeEventTypes_withEmptyMap_doesNothing() { + Map emptyMap = MapBuilder.builder().build(); + + UIManagerModuleConstantsHelper.normalizeEventTypes(emptyMap); + + assertTrue(emptyMap.isEmpty()); + } + + @Test + public void normalizeEventTypes_withOnEvent_doesNormalize() { + Map onClickMap = MapBuilder.builder().put("onClick", "¯\\_(ツ)_/¯").build(); + + UIManagerModuleConstantsHelper.normalizeEventTypes(onClickMap); + + assertTrue(onClickMap.containsKey("topOnClick")); + assertTrue(onClickMap.containsKey("onClick")); + } + + @Test + public void normalizeEventTypes_withTopEvent_doesNormalize() { + Map onClickMap = MapBuilder.builder().put("topOnClick", "¯\\_(ツ)_/¯").build(); + + UIManagerModuleConstantsHelper.normalizeEventTypes(onClickMap); + + assertTrue(onClickMap.containsKey("topOnClick")); + assertFalse(onClickMap.containsKey("onClick")); + } + + @SuppressWarnings("unchecked") + @Test + public void normalizeEventTypes_withNestedObjects_doesNotLoseThem() { + Map nestedObjects = + MapBuilder.builder() + .put( + "onColorChanged", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of( + "bubbled", "onColorChanged", "captured", "onColorChangedCapture"))) + .build(); + + UIManagerModuleConstantsHelper.normalizeEventTypes(nestedObjects); + + assertTrue(nestedObjects.containsKey("topOnColorChanged")); + Map innerMap = (Map) nestedObjects.get("topOnColorChanged"); + assertNotNull(innerMap); + assertTrue(innerMap.containsKey("phasedRegistrationNames")); + Map innerInnerMap = + (Map) innerMap.get("phasedRegistrationNames"); + assertNotNull(innerInnerMap); + assertEquals("onColorChanged", innerInnerMap.get("bubbled")); + assertEquals("onColorChangedCapture", innerInnerMap.get("captured")); + + assertTrue(nestedObjects.containsKey("onColorChanged")); + innerMap = (Map) nestedObjects.get("topOnColorChanged"); + assertNotNull(innerMap); + assertTrue(innerMap.containsKey("phasedRegistrationNames")); + innerInnerMap = (Map) innerMap.get("phasedRegistrationNames"); + assertNotNull(innerInnerMap); + assertEquals("onColorChanged", innerInnerMap.get("bubbled")); + assertEquals("onColorChangedCapture", innerInnerMap.get("captured")); + } +} diff --git a/packages/rn-tester/android/app/gradle.properties b/packages/rn-tester/android/app/gradle.properties index 838ca18c4c1caa..b6ce28ca151ced 100644 --- a/packages/rn-tester/android/app/gradle.properties +++ b/packages/rn-tester/android/app/gradle.properties @@ -13,6 +13,6 @@ android.enableJetifier=true FLIPPER_VERSION=0.182.0 # RN-Tester is building with NewArch always enabled -newArchEnabled=true +newArchEnabled=false # RN-Tester is running with Hermes enabled and filtering variants with enableHermesOnlyInVariants hermesEnabled=true