diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java index ddc87c40e4b1ce..8e7b79822104f7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/deviceinfo/DeviceInfoModule.java @@ -15,7 +15,7 @@ import com.facebook.react.bridge.ReactNoCrashSoftException; import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.bridge.WritableMap; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.uimanager.DisplayMetricsHolder; @@ -54,8 +54,13 @@ public String getName() { @Override public @Nullable Map getTypedExportedConstants() { + WritableMap displayMetrics = DisplayMetricsHolder.getDisplayMetricsWritableMap(mFontScale); + + // Cache the initial dimensions for later comparison in emitUpdateDimensionsEvent + mPreviousDisplayMetrics = displayMetrics.copy(); + HashMap constants = new HashMap<>(); - constants.put("Dimensions", DisplayMetricsHolder.getDisplayMetricsMap(mFontScale)); + constants.put("Dimensions", displayMetrics.toHashMap()); return constants; } @@ -85,8 +90,7 @@ public void emitUpdateDimensionsEvent() { if (mReactApplicationContext.hasActiveReactInstance()) { // Don't emit an event to JS if the dimensions haven't changed - WritableNativeMap displayMetrics = - DisplayMetricsHolder.getDisplayMetricsNativeMap(mFontScale); + WritableMap displayMetrics = DisplayMetricsHolder.getDisplayMetricsWritableMap(mFontScale); if (mPreviousDisplayMetrics == null) { mPreviousDisplayMetrics = displayMetrics.copy(); } else if (!displayMetrics.equals(mPreviousDisplayMetrics)) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java index 8ecd9cdf94221a..3caf9bcf44cb46 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/DisplayMetricsHolder.java @@ -13,9 +13,8 @@ import android.view.WindowManager; import androidx.annotation.Nullable; import com.facebook.infer.annotation.Assertions; +import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; -import java.util.HashMap; -import java.util.Map; /** * Holds an instance of the current DisplayMetrics so we don't have to thread it through all the @@ -81,40 +80,20 @@ public static DisplayMetrics getScreenDisplayMetrics() { return sScreenDisplayMetrics; } - public static Map> getDisplayMetricsMap(double fontScale) { + public static WritableMap getDisplayMetricsWritableMap(double fontScale) { Assertions.assertCondition( sWindowDisplayMetrics != null && sScreenDisplayMetrics != null, - "DisplayMetricsHolder must be initialized with initDisplayMetricsIfNotInitialized or initDisplayMetrics"); - final Map> result = new HashMap<>(); - result.put("windowPhysicalPixels", getPhysicalPixelsMap(sWindowDisplayMetrics, fontScale)); - result.put("screenPhysicalPixels", getPhysicalPixelsMap(sScreenDisplayMetrics, fontScale)); - return result; - } - - public static WritableNativeMap getDisplayMetricsNativeMap(double fontScale) { - Assertions.assertCondition( - sWindowDisplayMetrics != null && sScreenDisplayMetrics != null, - "DisplayMetricsHolder must be initialized with initDisplayMetricsIfNotInitialized or initDisplayMetrics"); + "DisplayMetricsHolder must be initialized with initDisplayMetricsIfNotInitialized or" + + " initDisplayMetrics"); final WritableNativeMap result = new WritableNativeMap(); result.putMap( - "windowPhysicalPixels", getPhysicalPixelsNativeMap(sWindowDisplayMetrics, fontScale)); + "windowPhysicalPixels", getPhysicalPixelsWritableMap(sWindowDisplayMetrics, fontScale)); result.putMap( - "screenPhysicalPixels", getPhysicalPixelsNativeMap(sScreenDisplayMetrics, fontScale)); - return result; - } - - private static Map getPhysicalPixelsMap( - DisplayMetrics displayMetrics, double fontScale) { - final Map result = new HashMap<>(); - result.put("width", displayMetrics.widthPixels); - result.put("height", displayMetrics.heightPixels); - result.put("scale", displayMetrics.density); - result.put("fontScale", fontScale); - result.put("densityDpi", displayMetrics.densityDpi); + "screenPhysicalPixels", getPhysicalPixelsWritableMap(sScreenDisplayMetrics, fontScale)); return result; } - private static WritableNativeMap getPhysicalPixelsNativeMap( + private static WritableMap getPhysicalPixelsWritableMap( DisplayMetrics displayMetrics, double fontScale) { final WritableNativeMap result = new WritableNativeMap(); result.putInt("width", displayMetrics.widthPixels); diff --git a/ReactAndroid/src/test/java/com/facebook/react/bridge/ReactTestHelper.java b/ReactAndroid/src/test/java/com/facebook/react/bridge/ReactTestHelper.java index ffce9ea596a157..d3ab1b7fe70e5b 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/bridge/ReactTestHelper.java +++ b/ReactAndroid/src/test/java/com/facebook/react/bridge/ReactTestHelper.java @@ -52,6 +52,7 @@ public void handleException(Exception e) { when(reactInstance.getReactQueueConfiguration()).thenReturn(ReactQueueConfiguration); when(reactInstance.getNativeModule(UIManagerModule.class)) .thenReturn(mock(UIManagerModule.class)); + when(reactInstance.isDestroyed()).thenReturn(false); return reactInstance; } diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK index 750600054fbf23..f50c34f262d139 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/BUCK @@ -32,6 +32,7 @@ rn_robolectric_test( react_native_target("java/com/facebook/react/modules/common:common"), react_native_target("java/com/facebook/react/modules/core:core"), react_native_target("java/com/facebook/react/modules/debug:debug"), + react_native_target("java/com/facebook/react/modules/deviceinfo:deviceinfo"), react_native_target("java/com/facebook/react/modules/dialog:dialog"), react_native_target("java/com/facebook/react/modules/network:network"), react_native_target("java/com/facebook/react/modules/share:share"), diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/deviceinfo/DeviceInfoModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/deviceinfo/DeviceInfoModuleTest.java new file mode 100644 index 00000000000000..6ed814abb53406 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/deviceinfo/DeviceInfoModuleTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) Facebook, Inc. and its 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.modules.deviceinfo; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactTestHelper; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.uimanager.DisplayMetricsHolder; +import java.util.Arrays; +import java.util.List; +import junit.framework.TestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@PrepareForTest({Arguments.class, DisplayMetricsHolder.class}) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "androidx.*", "android.*"}) +public class DeviceInfoModuleTest extends TestCase { + + @Rule public PowerMockRule rule = new PowerMockRule(); + + private DeviceInfoModule mDeviceInfoModule; + private DeviceEventManagerModule.RCTDeviceEventEmitter mRCTDeviceEventEmitterMock; + + private WritableMap fakePortraitDisplayMetrics; + private WritableMap fakeLandscapeDisplayMetrics; + + @Before + public void setUp() { + initTestData(); + + mockStatic(DisplayMetricsHolder.class); + + mRCTDeviceEventEmitterMock = mock(DeviceEventManagerModule.RCTDeviceEventEmitter.class); + + final ReactApplicationContext context = + spy(new ReactApplicationContext(RuntimeEnvironment.application)); + CatalystInstance catalystInstanceMock = ReactTestHelper.createMockCatalystInstance(); + context.initializeWithInstance(catalystInstanceMock); + when(context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)) + .thenReturn(mRCTDeviceEventEmitterMock); + + mDeviceInfoModule = new DeviceInfoModule(context); + } + + @After + public void teardown() { + DisplayMetricsHolder.setWindowDisplayMetrics(null); + DisplayMetricsHolder.setScreenDisplayMetrics(null); + } + + @Test + public void test_itDoesNotEmitAnEvent_whenDisplayMetricsNotChanged() { + givenDisplayMetricsHolderContains(fakePortraitDisplayMetrics); + + mDeviceInfoModule.getTypedExportedConstants(); + mDeviceInfoModule.emitUpdateDimensionsEvent(); + + verifyNoMoreInteractions(mRCTDeviceEventEmitterMock); + } + + @Test + public void test_itEmitsOneEvent_whenDisplayMetricsChangedOnce() { + givenDisplayMetricsHolderContains(fakePortraitDisplayMetrics); + + mDeviceInfoModule.getTypedExportedConstants(); + givenDisplayMetricsHolderContains(fakeLandscapeDisplayMetrics); + mDeviceInfoModule.emitUpdateDimensionsEvent(); + + verifyUpdateDimensionsEventsEmitted(mRCTDeviceEventEmitterMock, fakeLandscapeDisplayMetrics); + } + + @Test + public void test_itEmitsJustOneEvent_whenUpdateRequestedMultipleTimes() { + givenDisplayMetricsHolderContains(fakePortraitDisplayMetrics); + mDeviceInfoModule.getTypedExportedConstants(); + givenDisplayMetricsHolderContains(fakeLandscapeDisplayMetrics); + mDeviceInfoModule.emitUpdateDimensionsEvent(); + mDeviceInfoModule.emitUpdateDimensionsEvent(); + + verifyUpdateDimensionsEventsEmitted(mRCTDeviceEventEmitterMock, fakeLandscapeDisplayMetrics); + } + + @Test + public void test_itEmitsMultipleEvents_whenDisplayMetricsChangedBetweenUpdates() { + givenDisplayMetricsHolderContains(fakePortraitDisplayMetrics); + + mDeviceInfoModule.getTypedExportedConstants(); + mDeviceInfoModule.emitUpdateDimensionsEvent(); + givenDisplayMetricsHolderContains(fakeLandscapeDisplayMetrics); + mDeviceInfoModule.emitUpdateDimensionsEvent(); + givenDisplayMetricsHolderContains(fakePortraitDisplayMetrics); + mDeviceInfoModule.emitUpdateDimensionsEvent(); + givenDisplayMetricsHolderContains(fakeLandscapeDisplayMetrics); + mDeviceInfoModule.emitUpdateDimensionsEvent(); + + verifyUpdateDimensionsEventsEmitted( + mRCTDeviceEventEmitterMock, + fakeLandscapeDisplayMetrics, + fakePortraitDisplayMetrics, + fakeLandscapeDisplayMetrics); + } + + private static void givenDisplayMetricsHolderContains(final WritableMap fakeDisplayMetrics) { + when(DisplayMetricsHolder.getDisplayMetricsWritableMap(1.0)).thenReturn(fakeDisplayMetrics); + } + + private static void verifyUpdateDimensionsEventsEmitted( + DeviceEventManagerModule.RCTDeviceEventEmitter emitter, WritableMap... expectedEvents) { + List expectedEventList = Arrays.asList(expectedEvents); + ArgumentCaptor captor = ArgumentCaptor.forClass(WritableMap.class); + verify(emitter, times(expectedEventList.size())) + .emit(eq("didUpdateDimensions"), captor.capture()); + + List actualEvents = captor.getAllValues(); + assertThat(actualEvents).isEqualTo(expectedEventList); + } + + private void initTestData() { + mockStatic(Arguments.class); + when(Arguments.createMap()) + .thenAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new JavaOnlyMap(); + } + }); + + fakePortraitDisplayMetrics = Arguments.createMap(); + fakePortraitDisplayMetrics.putInt("width", 100); + fakePortraitDisplayMetrics.putInt("height", 200); + + fakeLandscapeDisplayMetrics = Arguments.createMap(); + fakeLandscapeDisplayMetrics.putInt("width", 200); + fakeLandscapeDisplayMetrics.putInt("height", 100); + } +}