From 83fd314e375b182551f33ca5c9f7b7125ae1e86b Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Wed, 27 Jan 2016 07:26:55 -0800 Subject: [PATCH] Moving more unit tests to open source Reviewed By: foghina, mkonicek Differential Revision: D2865561 fb-gh-sync-id: 7aab67d205b94d657a6ec7bf81a4ecee644eabc2 --- .../java/robolectric3/robolectric/BUCK | 3 - .../java/com/facebook/react/uimanager/BUCK | 37 + .../LayoutPropertyApplicatorTest.java | 449 ++++++++++ .../ReactPropAnnotationSetterSpecTest.java | 143 +++ .../ReactPropAnnotationSetterTest.java | 493 +++++++++++ .../uimanager/ReactPropConstantsTest.java | 188 ++++ .../ReactPropForShadowNodeSetterTest.java | 133 +++ .../ReactPropForShadowNodeSpecTest.java | 123 +++ .../uimanager/SimpleViewPropertyTest.java | 105 +++ .../UIManagerModuleConstantsTest.java | 198 +++++ .../react/uimanager/UIManagerModuleTest.java | 827 ++++++++++++++++++ circle.yml | 12 +- 12 files changed, 2702 insertions(+), 9 deletions(-) create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/BUCK create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/LayoutPropertyApplicatorTest.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterSpecTest.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterTest.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropConstantsTest.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSetterTest.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSpecTest.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/SimpleViewPropertyTest.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsTest.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleTest.java diff --git a/ReactAndroid/src/main/third-party/java/robolectric3/robolectric/BUCK b/ReactAndroid/src/main/third-party/java/robolectric3/robolectric/BUCK index b035e0941..1f5d94f76 100644 --- a/ReactAndroid/src/main/third-party/java/robolectric3/robolectric/BUCK +++ b/ReactAndroid/src/main/third-party/java/robolectric3/robolectric/BUCK @@ -17,9 +17,6 @@ android_library( react_native_dep('third-party/java/asm:asm'), react_native_dep('third-party/java/sqlite:sqlite'), ], - deps = [ -# ':locate_android-all', - ], visibility = ['//ReactAndroid/...',], ) diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/BUCK b/ReactAndroid/src/test/java/com/facebook/react/uimanager/BUCK new file mode 100644 index 000000000..9fe1b6977 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/BUCK @@ -0,0 +1,37 @@ +include_defs('//ReactAndroid/DEFS') + +robolectric3_test( + name = 'uimanager', + # Please change the contact to the oncall of your team + contacts = ['oncall+fbandroid_sheriff@xmail.facebook.com'], + srcs = glob(['**/*.java']), + deps = [ + react_native_target('java/com/facebook/csslayout:csslayout'), + react_native_target('java/com/facebook/react:react'), + react_native_target('java/com/facebook/react/animation:animation'), + react_native_target('java/com/facebook/react/bridge:bridge'), + react_native_target('java/com/facebook/react/common:common'), + react_native_target('java/com/facebook/react/touch:touch'), + react_native_target('java/com/facebook/react/uimanager:uimanager'), + react_native_target('java/com/facebook/react/uimanager/annotations:annotations'), + react_native_target('java/com/facebook/react/views/text:text'), + react_native_target('java/com/facebook/react/views/view:view'), + + react_native_tests_target('java/com/facebook/react/bridge:testhelpers'), + react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), + react_native_dep('third-party/java/robolectric3/robolectric:robolectric'), + react_native_dep('third-party/java/fest:fest'), + react_native_dep('third-party/java/junit:junit'), + react_native_dep('third-party/java/okio:okio'), + react_native_dep('third-party/java/mockito:mockito'), + react_native_dep('third-party/java/okhttp:okhttp'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + ], + visibility = [ + 'PUBLIC' + ], +) + +project_config( + test_target = ':uimanager', +) diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/LayoutPropertyApplicatorTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/LayoutPropertyApplicatorTest.java new file mode 100644 index 000000000..155af711f --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/LayoutPropertyApplicatorTest.java @@ -0,0 +1,449 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.util.DisplayMetrics; + +import com.facebook.csslayout.CSSAlign; +import com.facebook.csslayout.CSSConstants; +import com.facebook.csslayout.CSSFlexDirection; +import com.facebook.csslayout.CSSJustify; +import com.facebook.csslayout.CSSPositionType; +import com.facebook.csslayout.Spacing; +import com.facebook.react.bridge.SimpleMap; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyFloat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +@PrepareForTest({PixelUtil.class}) +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class LayoutPropertyApplicatorTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Before + public void setup() { + DisplayMetricsHolder.setDisplayMetrics(new DisplayMetrics()); + } + + @After + public void teardown() { + DisplayMetricsHolder.setDisplayMetrics(null); + } + + public ReactStylesDiffMap buildStyles(Object... keysAndValues) { + return new ReactStylesDiffMap(SimpleMap.of(keysAndValues)); + } + + @Test + public void testDimensions() { + LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode()); + ReactStylesDiffMap map = spy( + buildStyles( + "width", 10.0, + "height", 10.0, + "left", 10.0, + "top", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setStyleWidth(anyFloat()); + verify(map).getFloat(eq("width"), anyFloat()); + verify(reactShadowNode).setStyleHeight(anyFloat()); + verify(map).getFloat(eq("height"), anyFloat()); + verify(reactShadowNode).setPositionLeft(anyFloat()); + verify(map).getFloat(eq("left"), anyFloat()); + verify(reactShadowNode).setPositionTop(anyFloat()); + verify(map).getFloat(eq("top"), anyFloat()); + + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles()); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode, never()).setStyleWidth(anyFloat()); + verify(map, never()).getFloat(eq("width"), anyFloat()); + verify(reactShadowNode, never()).setStyleHeight(anyFloat()); + verify(map, never()).getFloat(eq("height"), anyFloat()); + verify(reactShadowNode, never()).setPositionLeft(anyFloat()); + verify(map, never()).getFloat(eq("left"), anyFloat()); + verify(reactShadowNode, never()).setPositionTop(anyFloat()); + verify(map, never()).getFloat(eq("top"), anyFloat()); + } + + @Test + public void testFlex() { + LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode()); + ReactStylesDiffMap map = spy(buildStyles("flex", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setFlex(anyFloat()); + verify(map).getFloat("flex", 0.f); + + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles()); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode, never()).setFlex(anyFloat()); + verify(map, never()).getFloat("flex", 0.f); + } + + @Test + public void testPosition() { + LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode()); + ReactStylesDiffMap map = spy(buildStyles( + "position", "absolute", + "bottom", 10.0, + "right", 5.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setPositionBottom(anyFloat()); + verify(reactShadowNode).setPositionRight(anyFloat()); + verify(reactShadowNode).setPositionType(any(CSSPositionType.class)); + verify(map).getFloat("bottom", Float.NaN); + verify(map).getFloat("right", Float.NaN); + + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles()); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode, never()).setPositionBottom(anyFloat()); + verify(reactShadowNode, never()).setPositionRight(anyFloat()); + verify(reactShadowNode, never()).setPositionType(any(CSSPositionType.class)); + verify(map, never()).getFloat("bottom", Float.NaN); + verify(map, never()).getFloat("right", Float.NaN); + } + + @Test + public void testMargin() { + // margin + LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode()); + ReactStylesDiffMap map = spy(buildStyles("margin", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setMargin(eq(Spacing.ALL), anyFloat()); + verify(map).getFloat("margin", CSSConstants.UNDEFINED); + + // marginVertical + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("marginVertical", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setMargin(eq(Spacing.VERTICAL), anyFloat()); + verify(map).getFloat("marginVertical", CSSConstants.UNDEFINED); + + // marginHorizontal + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("marginHorizontal", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setMargin(eq(Spacing.HORIZONTAL), anyFloat()); + verify(map).getFloat("marginHorizontal", CSSConstants.UNDEFINED); + + // marginTop + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("marginTop", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setMargin(eq(Spacing.TOP), anyFloat()); + verify(map).getFloat("marginTop", CSSConstants.UNDEFINED); + + // marginBottom + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("marginBottom", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setMargin(eq(Spacing.BOTTOM), anyFloat()); + verify(map).getFloat("marginBottom", CSSConstants.UNDEFINED); + + // marginLeft + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("marginLeft", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setMargin(eq(Spacing.LEFT), anyFloat()); + verify(map).getFloat("marginLeft", CSSConstants.UNDEFINED); + + // marginRight + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("marginRight", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setMargin(eq(Spacing.RIGHT), anyFloat()); + verify(map).getFloat("marginRight", CSSConstants.UNDEFINED); + + // no margin + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles()); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode, never()).setMargin(anyInt(), anyFloat()); + verify(map, never()).getFloat("margin", CSSConstants.UNDEFINED); + } + + @Test + public void testPadding() { + // padding + LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode()); + ReactStylesDiffMap map = spy(buildStyles("padding", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setPadding(eq(Spacing.ALL), anyFloat()); + verify(map).getFloat("padding", CSSConstants.UNDEFINED); + + // paddingVertical + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("paddingVertical", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setPadding(eq(Spacing.VERTICAL), anyFloat()); + verify(map).getFloat("paddingVertical", CSSConstants.UNDEFINED); + + // paddingHorizontal + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("paddingHorizontal", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setPadding(eq(Spacing.HORIZONTAL), anyFloat()); + verify(map).getFloat("paddingHorizontal", CSSConstants.UNDEFINED); + + // paddingTop + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("paddingTop", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setPadding(eq(Spacing.TOP), anyFloat()); + verify(map).getFloat("paddingTop", CSSConstants.UNDEFINED); + + // paddingBottom + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("paddingBottom", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setPadding(eq(Spacing.BOTTOM), anyFloat()); + verify(map).getFloat("paddingBottom", CSSConstants.UNDEFINED); + + // paddingLeft + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("paddingLeft", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setPadding(eq(Spacing.LEFT), anyFloat()); + verify(map).getFloat("paddingLeft", CSSConstants.UNDEFINED); + + // paddingRight + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles("paddingRight", 10.0)); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setPadding(eq(Spacing.RIGHT), anyFloat()); + verify(map).getFloat("paddingRight", CSSConstants.UNDEFINED); + + // no padding + reactShadowNode = spy(new LayoutShadowNode()); + map = spy(buildStyles()); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode, never()).setPadding(anyInt(), anyFloat()); + verify(map, never()).getFloat("padding", CSSConstants.UNDEFINED); + } + + @Test + public void testEnumerations() { + LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode()); + ReactStylesDiffMap map = buildStyles( + "flexDirection", "column", + "alignSelf", "stretch", + "alignItems", "center", + "justifyContent", "space_between", + "position", "relative"); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setFlexDirection(CSSFlexDirection.COLUMN); + verify(reactShadowNode).setAlignSelf(CSSAlign.STRETCH); + verify(reactShadowNode).setAlignItems(CSSAlign.CENTER); + verify(reactShadowNode).setJustifyContent(CSSJustify.SPACE_BETWEEN); + verify(reactShadowNode).setPositionType(CSSPositionType.RELATIVE); + + reactShadowNode = spy(new LayoutShadowNode()); + map = buildStyles(); + reactShadowNode.updateProperties(map); + + verify(reactShadowNode, never()).setFlexDirection(any(CSSFlexDirection.class)); + verify(reactShadowNode, never()).setAlignSelf(any(CSSAlign.class)); + verify(reactShadowNode, never()).setAlignItems(any(CSSAlign.class)); + verify(reactShadowNode, never()).setJustifyContent(any(CSSJustify.class)); + verify(reactShadowNode, never()).setPositionType(any(CSSPositionType.class)); + } + + @Test + public void testPropertiesResetToDefault() { + DisplayMetrics displayMetrics = new DisplayMetrics(); + displayMetrics.density = 1.0f; + DisplayMetricsHolder.setDisplayMetrics(displayMetrics); + + LayoutShadowNode reactShadowNode = spy(new LayoutShadowNode()); + ReactStylesDiffMap map = buildStyles( + "width", 10.0, + "height", 10.0, + "left", 10.0, + "top", 10.0, + "flex", 1.0, + "padding", 10.0, + "marginLeft", 10.0, + "borderTopWidth", 10.0, + "flexDirection", "row", + "alignSelf", "stretch", + "alignItems", "center", + "justifyContent", "space_between", + "position", "absolute"); + + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setStyleWidth(10.f); + verify(reactShadowNode).setStyleHeight(10.f); + verify(reactShadowNode).setPositionLeft(10.f); + verify(reactShadowNode).setPositionTop(10.f); + verify(reactShadowNode).setFlex(1.0f); + verify(reactShadowNode).setPadding(Spacing.ALL, 10.f); + verify(reactShadowNode).setMargin(Spacing.LEFT, 10.f); + verify(reactShadowNode).setBorder(Spacing.TOP, 10.f); + verify(reactShadowNode).setFlexDirection(CSSFlexDirection.ROW); + verify(reactShadowNode).setAlignSelf(CSSAlign.STRETCH); + verify(reactShadowNode).setAlignItems(CSSAlign.CENTER); + verify(reactShadowNode).setJustifyContent(CSSJustify.SPACE_BETWEEN); + verify(reactShadowNode).setPositionType(CSSPositionType.ABSOLUTE); + + map = buildStyles( + "width", null, + "height", null, + "left", null, + "top", null, + "flex", null, + "padding", null, + "marginLeft", null, + "borderTopWidth", null, + "flexDirection", null, + "alignSelf", null, + "alignItems", null, + "justifyContent", null, + "position", null); + + reset(reactShadowNode); + reactShadowNode.updateProperties(map); + verify(reactShadowNode).setStyleWidth(CSSConstants.UNDEFINED); + verify(reactShadowNode).setStyleHeight(CSSConstants.UNDEFINED); + verify(reactShadowNode).setPositionLeft(CSSConstants.UNDEFINED); + verify(reactShadowNode).setPositionTop(CSSConstants.UNDEFINED); + verify(reactShadowNode).setFlex(0.f); + verify(reactShadowNode).setPadding(Spacing.ALL, CSSConstants.UNDEFINED); + verify(reactShadowNode).setMargin(Spacing.LEFT, CSSConstants.UNDEFINED); + verify(reactShadowNode).setBorder(Spacing.TOP, CSSConstants.UNDEFINED); + verify(reactShadowNode).setFlexDirection(CSSFlexDirection.COLUMN); + verify(reactShadowNode).setAlignSelf(CSSAlign.AUTO); + verify(reactShadowNode).setAlignItems(CSSAlign.STRETCH); + verify(reactShadowNode).setJustifyContent(CSSJustify.FLEX_START); + verify(reactShadowNode).setPositionType(CSSPositionType.RELATIVE); + } + + @Test + public void testSettingDefaultStyleValues() { + mockStatic(PixelUtil.class); + when(PixelUtil.toPixelFromDIP(anyFloat())).thenAnswer( + new Answer() { + @Override + public Float answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + return (Float) args[0]; + } + }); + + LayoutShadowNode[] nodes = new LayoutShadowNode[7]; + for (int idx = 0; idx < nodes.length; idx++) { + nodes[idx] = new LayoutShadowNode(); + nodes[idx].setDefaultPadding(Spacing.LEFT, 15); + nodes[idx].setDefaultPadding(Spacing.TOP, 25); + nodes[idx].setDefaultPadding(Spacing.RIGHT, 35); + nodes[idx].setDefaultPadding(Spacing.BOTTOM, 45); + } + + ReactStylesDiffMap[] mapNodes = new ReactStylesDiffMap[7]; + mapNodes[0] = buildStyles("paddingLeft", 10.0, "paddingHorizontal", 5.0); + mapNodes[1] = buildStyles("padding", 10.0, "paddingTop", 5.0); + mapNodes[2] = buildStyles("paddingLeft", 10.0, "paddingVertical", 5.0); + mapNodes[3] = buildStyles("paddingBottom", 10.0, "paddingHorizontal", 5.0); + mapNodes[4] = buildStyles("padding", null, "paddingTop", 5.0); + mapNodes[5] = buildStyles( + "paddingRight", 10.0, + "paddingHorizontal", null, + "paddingVertical", 7.0); + mapNodes[6] = buildStyles("margin", 5.0); + + for (int idx = 0; idx < nodes.length; idx++) { + nodes[idx].updateProperties(mapNodes[idx]); + } + + assertEquals(10.0, nodes[0].getPadding().get(Spacing.LEFT), .0001); + assertEquals(25.0, nodes[0].getPadding().get(Spacing.TOP), .0001); + assertEquals(5.0, nodes[0].getPadding().get(Spacing.RIGHT), .0001); + assertEquals(45.0, nodes[0].getPadding().get(Spacing.BOTTOM), .0001); + + assertEquals(10.0, nodes[1].getPadding().get(Spacing.LEFT), .0001); + assertEquals(5.0, nodes[1].getPadding().get(Spacing.TOP), .0001); + assertEquals(10.0, nodes[1].getPadding().get(Spacing.RIGHT), .0001); + assertEquals(10.0, nodes[1].getPadding().get(Spacing.BOTTOM), .0001); + + assertEquals(10.0, nodes[2].getPadding().get(Spacing.LEFT), .0001); + assertEquals(5.0, nodes[2].getPadding().get(Spacing.TOP), .0001); + assertEquals(35.0, nodes[2].getPadding().get(Spacing.RIGHT), .0001); + assertEquals(5.0, nodes[2].getPadding().get(Spacing.BOTTOM), .0001); + + assertEquals(5.0, nodes[3].getPadding().get(Spacing.LEFT), .0001); + assertEquals(25.0, nodes[3].getPadding().get(Spacing.TOP), .0001); + assertEquals(5.0, nodes[3].getPadding().get(Spacing.RIGHT), .0001); + assertEquals(10.0, nodes[3].getPadding().get(Spacing.BOTTOM), .0001); + + assertEquals(15.0, nodes[4].getPadding().get(Spacing.LEFT), .0001); + assertEquals(5.0, nodes[4].getPadding().get(Spacing.TOP), .0001); + assertEquals(35.0, nodes[4].getPadding().get(Spacing.RIGHT), .0001); + assertEquals(45.0, nodes[4].getPadding().get(Spacing.BOTTOM), .0001); + + assertEquals(15.0, nodes[5].getPadding().get(Spacing.LEFT), .0001); + assertEquals(7.0, nodes[5].getPadding().get(Spacing.TOP), .0001); + assertEquals(10.0, nodes[5].getPadding().get(Spacing.RIGHT), .0001); + assertEquals(7.0, nodes[5].getPadding().get(Spacing.BOTTOM), .0001); + + assertEquals(15.0, nodes[6].getPadding().get(Spacing.LEFT), .0001); + assertEquals(25.0, nodes[6].getPadding().get(Spacing.TOP), .0001); + assertEquals(35.0, nodes[6].getPadding().get(Spacing.RIGHT), .0001); + assertEquals(45.0, nodes[6].getPadding().get(Spacing.BOTTOM), .0001); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterSpecTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterSpecTest.java new file mode 100644 index 000000000..8df49c6f6 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterSpecTest.java @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.Date; + +import android.view.View; + +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.annotations.ReactPropGroup; + +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** + * Test that verifies that spec of methods annotated with @ReactProp is correct + */ +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ReactPropAnnotationSetterSpecTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private abstract class BaseViewManager extends ViewManager { + + @Override + public String getName() { + return "IgnoredName"; + } + + @Override + public ReactShadowNode createShadowNodeInstance() { + return null; + } + + @Override + public Class getShadowNodeClass() { + return ReactShadowNode.class; + } + + @Override + protected View createViewInstance(ThemedReactContext reactContext) { + return null; + } + + @Override + public void updateExtraData(View root, Object extraData) { + } + } + + @Test(expected = RuntimeException.class) + public void testMethodWithWongNumberOfParams() { + new BaseViewManager() { + @ReactProp(name = "prop") + public void setterWithIncorrectNumberOfArgs(View v, boolean value, boolean otherValue) { + } + }.getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testMethodWithTooFewParams() { + new BaseViewManager() { + @ReactProp(name = "prop") + public void setterWithTooFewParams(View v) { + } + }.getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testUnsupportedPropValueType() { + new BaseViewManager() { + @ReactProp(name = "prop") + public void setterWithUnsupportedValueType(View v, Date value) { + } + }.getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testSetterWIthNonViewParam() { + new BaseViewManager() { + @ReactProp(name = "prop") + public void setterWithNonViewParam(Object v, boolean value) { + } + }.getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testGroupInvalidNumberOfParams() { + new BaseViewManager() { + @ReactPropGroup(names = {"prop1", "prop2"}) + public void setterWIthInvalidNumberOfParams(View v, int index, float value, float other) { + } + }.getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testGroupTooFewParams() { + new BaseViewManager() { + @ReactPropGroup(names = {"prop1", "prop2"}) + public void setterWIthTooFewParams(View v, int index) { + } + }.getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testGroupNoIndexParam() { + new BaseViewManager() { + @ReactPropGroup(names = {"prop1", "prop2"}) + public void setterWithoutIndexParam(View v, float value, float sth) { + } + }.getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testGroupNoViewParam() { + new BaseViewManager() { + @ReactPropGroup(names = {"prop1", "prop2"}) + public void setterWithoutViewParam(Object v, int index, float value) { + } + }.getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testGroupUnsupportedPropType() { + new BaseViewManager() { + @ReactPropGroup(names = {"prop1", "prop2"}) + public void setterWithUnsupportedPropType(View v, int index, long value) { + } + }.getNativeProps(); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterTest.java new file mode 100644 index 000000000..88cbbf7d3 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterTest.java @@ -0,0 +1,493 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import android.view.View; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.SimpleArray; +import com.facebook.react.bridge.SimpleMap; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.annotations.ReactPropGroup; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.junit.Rule; +import org.junit.Test; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; + +import static org.fest.assertions.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Test updating view through {@link ViewManager} with {@link ReactProp} and {@link ReactPropGroup} + * annotations. + */ +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ReactPropAnnotationSetterTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + public interface ViewManagerUpdatesReceiver { + void onBooleanSetterCalled(boolean value); + void onIntSetterCalled(int value); + void onDoubleSetterCalled(double value); + void onFloatSetterCalled(float value); + void onStringSetterCalled(String value); + void onBoxedBooleanSetterCalled(Boolean value); + void onBoxedIntSetterCalled(Integer value); + void onArraySetterCalled(ReadableArray value); + void onMapSetterCalled(ReadableMap value); + void onFloatGroupPropSetterCalled(int index, float value); + void onIntGroupPropSetterCalled(int index, int value); + void onBoxedIntGroupPropSetterCalled(int index, Integer value); + } + + private class ViewManagerUnderTest extends ViewManager { + + final ViewManagerUpdatesReceiver mViewManagerUpdatesReceiver; + + private ViewManagerUnderTest(ViewManagerUpdatesReceiver viewManagerUpdatesReceiver) { + mViewManagerUpdatesReceiver = viewManagerUpdatesReceiver; + } + + @Override + public String getName() { + return "RedpandasLivestreamVideoView"; + } + + @Override + public ReactShadowNode createShadowNodeInstance() { + fail("This method should not be executed as a part of this test"); + return null; + } + + @Override + public Class getShadowNodeClass() { + return ReactShadowNode.class; + } + + @Override + protected View createViewInstance(ThemedReactContext reactContext) { + fail("This method should not be executed as a part of this test"); + return null; + } + + @Override + public void updateExtraData(View root, Object extraData) { + fail("This method should not be executed as a part of this test"); + } + + @ReactProp(name = "boolProp") + public void setBoolProp(View v, boolean value) { + mViewManagerUpdatesReceiver.onBooleanSetterCalled(value); + } + + @ReactProp(name = "boolPropWithDefault", defaultBoolean = true) + public void setBoolPropWithDefault(View v, boolean value) { + mViewManagerUpdatesReceiver.onBooleanSetterCalled(value); + } + + @ReactProp(name = "intProp") + public void setIntProp(View v, int value) { + mViewManagerUpdatesReceiver.onIntSetterCalled(value); + } + + @ReactProp(name = "intPropWithDefault", defaultInt = 7168) + public void setIntPropWithDefault(View v, int value) { + mViewManagerUpdatesReceiver.onIntSetterCalled(value); + } + + @ReactProp(name = "floatProp") + public void setFloatProp(View v, float value) { + mViewManagerUpdatesReceiver.onFloatSetterCalled(value); + } + + @ReactProp(name = "floatPropWithDefault", defaultFloat = 14.0f) + public void setFloatPropWithDefault(View v, float value) { + mViewManagerUpdatesReceiver.onFloatSetterCalled(value); + } + + @ReactProp(name = "doubleProp") + public void setDoubleProp(View v, double value) { + mViewManagerUpdatesReceiver.onDoubleSetterCalled(value); + } + + @ReactProp(name = "doublePropWithDefault", defaultDouble = -88.0) + public void setDoublePropWithDefault(View v, double value) { + mViewManagerUpdatesReceiver.onDoubleSetterCalled(value); + } + + @ReactProp(name = "stringProp") + public void setStringProp(View v, String value) { + mViewManagerUpdatesReceiver.onStringSetterCalled(value); + } + + @ReactProp(name = "boxedBoolProp") + public void setBoxedBoolProp(View v, Boolean value) { + mViewManagerUpdatesReceiver.onBoxedBooleanSetterCalled(value); + } + + @ReactProp(name = "boxedIntProp") + public void setBoxedIntProp(View v, Integer value) { + mViewManagerUpdatesReceiver.onBoxedIntSetterCalled(value); + } + + @ReactProp(name = "arrayProp") + public void setArrayProp(View v, ReadableArray value) { + mViewManagerUpdatesReceiver.onArraySetterCalled(value); + } + + @ReactProp(name = "mapProp") + public void setMapProp(View v, ReadableMap value) { + mViewManagerUpdatesReceiver.onMapSetterCalled(value); + } + + @ReactPropGroup(names = { + "floatGroupPropFirst", + "floatGroupPropSecond", + }) + public void setFloatGroupProp(View v, int index, float value) { + mViewManagerUpdatesReceiver.onFloatGroupPropSetterCalled(index, value); + } + + @ReactPropGroup(names = { + "floatGroupPropWithDefaultFirst", + "floatGroupPropWithDefaultSecond", + }, defaultFloat = -100.0f) + public void setFloatGroupPropWithDefault(View v, int index, float value) { + mViewManagerUpdatesReceiver.onFloatGroupPropSetterCalled(index, value); + } + + @ReactPropGroup(names = { + "intGroupPropFirst", + "intGroupPropSecond" + }) + public void setIntGroupProp(View v, int index, int value) { + mViewManagerUpdatesReceiver.onIntGroupPropSetterCalled(index, value); + } + + @ReactPropGroup(names = { + "intGroupPropWithDefaultFirst", + "intGroupPropWithDefaultSecond" + }, defaultInt = 555) + public void setIntGroupPropWithDefault(View v, int index, int value) { + mViewManagerUpdatesReceiver.onIntGroupPropSetterCalled(index, value); + } + + @ReactPropGroup(names = { + "boxedIntGroupPropFirst", + "boxedIntGroupPropSecond", + }) + public void setBoxedIntGroupProp(View v, int index, Integer value) { + mViewManagerUpdatesReceiver.onBoxedIntGroupPropSetterCalled(index, value); + } + } + + public static ReactStylesDiffMap buildStyles(Object... keysAndValues) { + return new ReactStylesDiffMap(SimpleMap.of(keysAndValues)); + } + + private ViewManagerUnderTest mViewManager; + private ViewManagerUpdatesReceiver mUpdatesReceiverMock; + + @Before + public void setup() { + mUpdatesReceiverMock = mock(ViewManagerUpdatesReceiver.class); + mViewManager = new ViewManagerUnderTest(mUpdatesReceiverMock); + } + + @Test + public void testBooleanSetter() { + mViewManager.updateProperties(null, buildStyles("boolProp", true)); + verify(mUpdatesReceiverMock).onBooleanSetterCalled(true); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("boolProp", false)); + verify(mUpdatesReceiverMock).onBooleanSetterCalled(false); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("boolProp", null)); + verify(mUpdatesReceiverMock).onBooleanSetterCalled(false); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("boolPropWithDefault", false)); + verify(mUpdatesReceiverMock).onBooleanSetterCalled(false); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("boolPropWithDefault", null)); + verify(mUpdatesReceiverMock).onBooleanSetterCalled(true); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testIntSetter() { + mViewManager.updateProperties(null, buildStyles("intProp", 13)); + verify(mUpdatesReceiverMock).onIntSetterCalled(13); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("intProp", null)); + verify(mUpdatesReceiverMock).onIntSetterCalled(0); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("intPropWithDefault", -1)); + verify(mUpdatesReceiverMock).onIntSetterCalled(-1); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("intPropWithDefault", null)); + verify(mUpdatesReceiverMock).onIntSetterCalled(7168); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testDoubleSetter() { + mViewManager.updateProperties(null, buildStyles("doubleProp", 13.0)); + verify(mUpdatesReceiverMock).onDoubleSetterCalled(13.0); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("doubleProp", null)); + verify(mUpdatesReceiverMock).onDoubleSetterCalled(0.0); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("doublePropWithDefault", -1.0)); + verify(mUpdatesReceiverMock).onDoubleSetterCalled(-1.0); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("doublePropWithDefault", null)); + verify(mUpdatesReceiverMock).onDoubleSetterCalled(-88.0); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testFloatSetter() { + mViewManager.updateProperties(null, buildStyles("floatProp", 13.0)); + verify(mUpdatesReceiverMock).onFloatSetterCalled(13.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("floatProp", null)); + verify(mUpdatesReceiverMock).onFloatSetterCalled(0.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("floatPropWithDefault", -1.0)); + verify(mUpdatesReceiverMock).onFloatSetterCalled(-1.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("floatPropWithDefault", null)); + verify(mUpdatesReceiverMock).onFloatSetterCalled(14.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testStringSetter() { + mViewManager.updateProperties(null, buildStyles("stringProp", "someRandomString")); + verify(mUpdatesReceiverMock).onStringSetterCalled("someRandomString"); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("stringProp", null)); + verify(mUpdatesReceiverMock).onStringSetterCalled(null); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testBoxedBooleanSetter() { + mViewManager.updateProperties(null, buildStyles("boxedBoolProp", true)); + verify(mUpdatesReceiverMock).onBoxedBooleanSetterCalled(Boolean.TRUE); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("boxedBoolProp", false)); + verify(mUpdatesReceiverMock).onBoxedBooleanSetterCalled(Boolean.FALSE); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("boxedBoolProp", null)); + verify(mUpdatesReceiverMock).onBoxedBooleanSetterCalled(null); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testBoxedIntSetter() { + mViewManager.updateProperties(null, buildStyles("boxedIntProp", 55)); + verify(mUpdatesReceiverMock).onBoxedIntSetterCalled(Integer.valueOf(55)); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("boxedIntProp", null)); + verify(mUpdatesReceiverMock).onBoxedIntSetterCalled(null); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testArraySetter() { + ReadableArray array = new SimpleArray(); + mViewManager.updateProperties(null, buildStyles("arrayProp", array)); + verify(mUpdatesReceiverMock).onArraySetterCalled(array); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("arrayProp", null)); + verify(mUpdatesReceiverMock).onArraySetterCalled(null); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testMapSetter() { + ReadableMap map = new SimpleMap(); + mViewManager.updateProperties(null, buildStyles("mapProp", map)); + verify(mUpdatesReceiverMock).onMapSetterCalled(map); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("mapProp", null)); + verify(mUpdatesReceiverMock).onMapSetterCalled(null); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testFloatGroupSetter() { + mViewManager.updateProperties(null, buildStyles("floatGroupPropFirst", 11.0)); + verify(mUpdatesReceiverMock).onFloatGroupPropSetterCalled(0, 11.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("floatGroupPropSecond", -111.0)); + verify(mUpdatesReceiverMock).onFloatGroupPropSetterCalled(1, -111.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("floatGroupPropSecond", null)); + verify(mUpdatesReceiverMock).onFloatGroupPropSetterCalled(1, 0.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("floatGroupPropWithDefaultFirst", null)); + verify(mUpdatesReceiverMock).onFloatGroupPropSetterCalled(0, -100.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testIntGroupSetter() { + mViewManager.updateProperties(null, buildStyles("intGroupPropFirst", -7)); + verify(mUpdatesReceiverMock).onIntGroupPropSetterCalled(0, -7); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("intGroupPropSecond", -77)); + verify(mUpdatesReceiverMock).onIntGroupPropSetterCalled(1, -77); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("intGroupPropSecond", null)); + verify(mUpdatesReceiverMock).onIntGroupPropSetterCalled(1, 0); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("intGroupPropWithDefaultFirst", 5)); + verify(mUpdatesReceiverMock).onIntGroupPropSetterCalled(0, 5); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("intGroupPropWithDefaultFirst", null)); + verify(mUpdatesReceiverMock).onIntGroupPropSetterCalled(0, 555); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("intGroupPropWithDefaultSecond", null)); + verify(mUpdatesReceiverMock).onIntGroupPropSetterCalled(1, 555); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testStringGroupSetter() { + mViewManager.updateProperties(null, buildStyles("boxedIntGroupPropFirst", -7)); + verify(mUpdatesReceiverMock).onBoxedIntGroupPropSetterCalled(0, -7); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("boxedIntGroupPropSecond", 12345)); + verify(mUpdatesReceiverMock).onBoxedIntGroupPropSetterCalled(1, 12345); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mViewManager.updateProperties(null, buildStyles("boxedIntGroupPropSecond", null)); + verify(mUpdatesReceiverMock).onBoxedIntGroupPropSetterCalled(1, null); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test(expected = JSApplicationIllegalArgumentException.class) + public void testFailToUpdateBoolPropWithMap() { + mViewManager.updateProperties(null, buildStyles("boolProp", new SimpleMap())); + } + + @Test(expected = JSApplicationIllegalArgumentException.class) + public void testFailToUpdateStringPropWithDouble() { + mViewManager.updateProperties(null, buildStyles("stringProp", 14.5)); + } + + @Test(expected = JSApplicationIllegalArgumentException.class) + public void testFailToUpdateDoublePropWithString() { + mViewManager.updateProperties(null, buildStyles("doubleProp", "hello")); + } + + @Test(expected = JSApplicationIllegalArgumentException.class) + public void testFailToUpdateIntPropWithDouble() { + mViewManager.updateProperties(null, buildStyles("intProp", -7.4)); + } + + @Test(expected = JSApplicationIllegalArgumentException.class) + public void testFailToUpdateArrayPropWithBool() { + mViewManager.updateProperties(null, buildStyles("arrayProp", false)); + } + + @Test(expected = JSApplicationIllegalArgumentException.class) + public void testFailToUpdateMapPropWithArray() { + mViewManager.updateProperties(null, buildStyles("mapProp", new SimpleArray())); + } + + @Test(expected = JSApplicationIllegalArgumentException.class) + public void testFailToUpdateBoxedIntPropWithBoxedDouble() { + mViewManager.updateProperties(null, buildStyles("boxedIntProp", Double.valueOf(1))); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropConstantsTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropConstantsTest.java new file mode 100644 index 000000000..2f6eea4b7 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropConstantsTest.java @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import android.view.View; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.annotations.ReactPropGroup; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.fest.assertions.api.Assertions.fail; + +/** + * Verifies that prop constants are generated properly based on {@code ReactProp} annotation. + */ +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ReactPropConstantsTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private class ViewManagerUnderTest extends ViewManager { + + @Override + public String getName() { + return "SomeView"; + } + + @Override + public ReactShadowNode createShadowNodeInstance() { + fail("This method should not be executed as a part of this test"); + return null; + } + + @Override + protected View createViewInstance(ThemedReactContext reactContext) { + fail("This method should not be executed as a part of this test"); + return null; + } + + @Override + public Class getShadowNodeClass() { + return ReactShadowNode.class; + } + + @Override + public void updateExtraData(View root, Object extraData) { + fail("This method should not be executed as a part of this test"); + } + + @ReactProp(name = "boolProp") + public void setBoolProp(View v, boolean value) { + } + + @ReactProp(name = "intProp") + public void setIntProp(View v, int value) { + } + + @ReactProp(name = "floatProp") + public void setFloatProp(View v, float value) { + } + + @ReactProp(name = "doubleProp") + public void setDoubleProp(View v, double value) { + } + + @ReactProp(name = "stringProp") + public void setStringProp(View v, String value) { + } + + @ReactProp(name = "boxedBoolProp") + public void setBoxedBoolProp(View v, Boolean value) { + } + + @ReactProp(name = "boxedIntProp") + public void setBoxedIntProp(View v, Integer value) { + } + + @ReactProp(name = "arrayProp") + public void setArrayProp(View v, ReadableArray value) { + } + + @ReactProp(name = "mapProp") + public void setMapProp(View v, ReadableMap value) { + } + + @ReactPropGroup(names = { + "floatGroupPropFirst", + "floatGroupPropSecond", + }) + public void setFloatGroupProp(View v, int index, float value) { + } + + @ReactPropGroup(names = { + "intGroupPropFirst", + "intGroupPropSecond" + }) + public void setIntGroupProp(View v, int index, int value) { + } + + @ReactPropGroup(names = { + "boxedIntGroupPropFirst", + "boxedIntGroupPropSecond", + }) + public void setBoxedIntGroupProp(View v, int index, Integer value) { + } + + @ReactProp(name = "customIntProp", customType = "date") + public void customIntProp(View v, int value) { + } + + @ReactPropGroup(names = { + "customBoxedIntGroupPropFirst", + "customBoxedIntGroupPropSecond" + }, customType = "color") + public void customIntGroupProp(View v, int index, Integer value) { + } + } + + @Test + public void testNativePropsIncludeCorrectTypes() { + List viewManagers = Arrays.asList(new ViewManagerUnderTest()); + ReactApplicationContext reactContext = new ReactApplicationContext(RuntimeEnvironment.application); + UIManagerModule uiManagerModule = new UIManagerModule( + reactContext, + viewManagers, + new UIImplementation(reactContext, viewManagers)); + Map constants = + (Map) valueAtPath(uiManagerModule.getConstants(), "SomeView", "NativeProps"); + assertThat(constants).isEqualTo( + MapBuilder.builder() + .put("boolProp", "boolean") + .put("intProp", "number") + .put("doubleProp", "number") + .put("floatProp", "number") + .put("stringProp", "String") + .put("boxedBoolProp", "boolean") + .put("boxedIntProp", "number") + .put("arrayProp", "Array") + .put("mapProp", "Map") + .put("floatGroupPropFirst", "number") + .put("floatGroupPropSecond", "number") + .put("intGroupPropFirst", "number") + .put("intGroupPropSecond", "number") + .put("boxedIntGroupPropFirst", "number") + .put("boxedIntGroupPropSecond", "number") + .put("customIntProp", "date") + .put("customBoxedIntGroupPropFirst", "color") + .put("customBoxedIntGroupPropSecond", "color") + .build()); + } + + private static Object valueAtPath(Map nestedMap, String... keyPath) { + assertThat(keyPath).isNotEmpty(); + Object value = nestedMap; + for (String key : keyPath) { + assertThat(value).isInstanceOf(Map.class); + nestedMap = (Map) value; + assertThat(nestedMap).containsKey(key); + value = nestedMap.get(key); + } + return value; + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSetterTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSetterTest.java new file mode 100644 index 000000000..59710b7ef --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSetterTest.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; + +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.annotations.ReactPropGroup; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.junit.Rule; +import org.junit.Test; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; + +import static com.facebook.react.uimanager.ReactPropAnnotationSetterTest.ViewManagerUpdatesReceiver; +import static com.facebook.react.uimanager.ReactPropAnnotationSetterTest.buildStyles; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Test {@link ReactProp} annotation for {@link ReactShadowNode}. More comprahensive test of this + * annotation can be found in {@link ReactPropAnnotationSetterTest} where we test all possible types + * of properties to be updated. + */ +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ReactPropForShadowNodeSetterTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private class ShadowViewUnderTest extends ReactShadowNode { + + private ViewManagerUpdatesReceiver mViewManagerUpdatesReceiver; + + private ShadowViewUnderTest(ViewManagerUpdatesReceiver viewManagerUpdatesReceiver) { + mViewManagerUpdatesReceiver = viewManagerUpdatesReceiver; + } + + @ReactProp(name = "boolProp") + public void setBoolProp(boolean value) { + mViewManagerUpdatesReceiver.onBooleanSetterCalled(value); + } + + @ReactProp(name = "stringProp") + public void setStringProp(@Nullable String value) { + mViewManagerUpdatesReceiver.onStringSetterCalled(value); + } + + @ReactProp(name = "boxedIntProp") + public void setBoxedIntProp(@Nullable Integer value) { + mViewManagerUpdatesReceiver.onBoxedIntSetterCalled(value); + } + + @ReactPropGroup(names = { + "floatGroupPropFirst", + "floatGroupPropSecond", + }) + public void setFloatGroupProp(int index, float value) { + mViewManagerUpdatesReceiver.onFloatGroupPropSetterCalled(index, value); + } + } + + private ViewManagerUpdatesReceiver mUpdatesReceiverMock; + private ShadowViewUnderTest mShadowView; + + @Before + public void setup() { + mUpdatesReceiverMock = mock(ViewManagerUpdatesReceiver.class); + mShadowView = new ShadowViewUnderTest(mUpdatesReceiverMock); + } + + @Test + public void testBooleanSetter() { + mShadowView.updateProperties(buildStyles("boolProp", true)); + verify(mUpdatesReceiverMock).onBooleanSetterCalled(true); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mShadowView.updateProperties(buildStyles("boolProp", false)); + verify(mUpdatesReceiverMock).onBooleanSetterCalled(false); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mShadowView.updateProperties(buildStyles("boolProp", null)); + verify(mUpdatesReceiverMock).onBooleanSetterCalled(false); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testStringSetter() { + mShadowView.updateProperties(buildStyles("stringProp", "someRandomString")); + verify(mUpdatesReceiverMock).onStringSetterCalled("someRandomString"); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mShadowView.updateProperties(buildStyles("stringProp", null)); + verify(mUpdatesReceiverMock).onStringSetterCalled(null); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } + + @Test + public void testFloatGroupSetter() { + mShadowView.updateProperties(buildStyles("floatGroupPropFirst", 11.0)); + verify(mUpdatesReceiverMock).onFloatGroupPropSetterCalled(0, 11.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mShadowView.updateProperties(buildStyles("floatGroupPropSecond", -111.0)); + verify(mUpdatesReceiverMock).onFloatGroupPropSetterCalled(1, -111.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + + mShadowView.updateProperties(buildStyles("floatGroupPropSecond", null)); + verify(mUpdatesReceiverMock).onFloatGroupPropSetterCalled(1, 0.0f); + verifyNoMoreInteractions(mUpdatesReceiverMock); + reset(mUpdatesReceiverMock); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSpecTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSpecTest.java new file mode 100644 index 000000000..1ddcdf047 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropForShadowNodeSpecTest.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.Map; + +import android.view.View; + +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.annotations.ReactPropGroup; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; + +/** + * Test that verifies that spec of methods annotated with @ReactProp in {@link ReactShadowNode} is + * correct + */ +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ReactPropForShadowNodeSpecTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private static class BaseViewManager extends ViewManager { + + private final Class mShadowNodeClass; + + private BaseViewManager(Class shadowNodeClass) { + mShadowNodeClass = shadowNodeClass; + } + + @Override + public String getName() { + return "IgnoredName"; + } + + @Override + public ReactShadowNode createShadowNodeInstance() { + return null; + } + + @Override + public Class getShadowNodeClass() { + return mShadowNodeClass; + } + + @Override + protected View createViewInstance(ThemedReactContext reactContext) { + return null; + } + + @Override + public void updateExtraData(View root, Object extraData) { + } + } + + @Test(expected = RuntimeException.class) + public void testMethodWithWrongNumberOfParams() { + new BaseViewManager(new ReactShadowNode() { + @ReactProp(name = "prop") + public void setterWithIncorrectNumberOfArgs(boolean value, int anotherValue) { + } + }.getClass()).getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testMethodWithTooFewParams() { + new BaseViewManager(new ReactShadowNode() { + @ReactProp(name = "prop") + public void setterWithNoArgs() { + } + }.getClass()).getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testUnsupportedValueType() { + new BaseViewManager(new ReactShadowNode() { + @ReactProp(name = "prop") + public void setterWithMap(Map value) { + } + }.getClass()).getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testGroupInvalidNumberOfParams() { + new BaseViewManager(new ReactShadowNode() { + @ReactPropGroup(names = {"prop1", "prop2"}) + public void setterWithTooManyParams(int index, float value, boolean bool) { + } + }.getClass()).getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testGroupTooFewParams() { + new BaseViewManager(new ReactShadowNode() { + @ReactPropGroup(names = {"prop1", "prop2"}) + public void setterWithTooManyParams(int index) { + } + }.getClass()).getNativeProps(); + } + + @Test(expected = RuntimeException.class) + public void testGroupNoIndexParam() { + new BaseViewManager(new ReactShadowNode() { + @ReactPropGroup(names = {"prop1", "prop2"}) + public void setterWithTooManyParams(float value, boolean bool) { + } + }.getClass()).getNativeProps(); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/SimpleViewPropertyTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/SimpleViewPropertyTest.java new file mode 100644 index 000000000..76f2b2e21 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/SimpleViewPropertyTest.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.Map; + +import android.view.View; + +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.touch.JSResponderHandler; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactTestHelper; +import com.facebook.react.bridge.SimpleMap; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.uimanager.annotations.ReactProp; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.RobolectricTestRunner; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.fest.assertions.api.Assertions.offset; + +/** + * Verify {@link View} view property being applied properly by {@link SimpleViewManager} + */ +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class SimpleViewPropertyTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private static class ConcreteViewManager extends SimpleViewManager { + + @ReactProp(name = "foo") + public void setFoo(View view, boolean foo) { + } + + @ReactProp(name = "bar") + public void setBar(View view, ReadableMap bar) { + } + + @Override + protected View createViewInstance(ThemedReactContext reactContext) { + return new View(reactContext); + } + + @Override + public String getName() { + return "View"; + } + } + + private ReactApplicationContext mContext; + private CatalystInstance mCatalystInstanceMock; + private ThemedReactContext mThemedContext; + private ConcreteViewManager mManager; + + @Before + public void setup() { + mContext = new ReactApplicationContext(RuntimeEnvironment.application); + mCatalystInstanceMock = ReactTestHelper.createMockCatalystInstance(); + mContext.initializeWithInstance(mCatalystInstanceMock); + mThemedContext = new ThemedReactContext(mContext, mContext); + mManager = new ConcreteViewManager(); + } + + public ReactStylesDiffMap buildStyles(Object... keysAndValues) { + return new ReactStylesDiffMap(SimpleMap.of(keysAndValues)); + } + + @Test + public void testOpacity() { + View view = mManager.createView(mThemedContext, new JSResponderHandler()); + + mManager.updateProperties(view, buildStyles()); + assertThat(view.getAlpha()).isEqualTo(1.0f); + + mManager.updateProperties(view, buildStyles("opacity", 0.31)); + assertThat(view.getAlpha()).isEqualTo(0.31f, offset(1e-5f)); + + mManager.updateProperties(view, buildStyles("opacity", null)); + assertThat(view.getAlpha()).isEqualTo(1.0f); + } + + @Test + public void testGetNativeProps() { + Map nativeProps = mManager.getNativeProps(); + assertThat(nativeProps.get("foo")).isEqualTo("boolean"); + assertThat(nativeProps.get("bar")).isEqualTo("Map"); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsTest.java new file mode 100644 index 000000000..5d757eb2f --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleConstantsTest.java @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.common.MapBuilder; + +import org.fest.assertions.data.MapEntry; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class UIManagerModuleConstantsTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private static final String CUSTOM_BUBBLING_EVENT_TYPES = "customBubblingEventTypes"; + private static final String CUSTOM_DIRECT_EVENT_TYPES = "customDirectEventTypes"; + + private static final Map TWIRL_BUBBLING_EVENT_MAP = MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of( + "bubbled", + "onTwirl", + "captured", + "onTwirlCaptured")); + private static final Map TWIRL_DIRECT_EVENT_MAP = MapBuilder.of("registrationName", "onTwirl"); + + private ReactApplicationContext mReactContext; + private UIImplementation mUIImplementation; + + @Before + public void setUp() { + mReactContext = new ReactApplicationContext(RuntimeEnvironment.application); + mUIImplementation = mock(UIImplementation.class); + } + + @Test + public void testNoCustomConstants() { + List viewManagers = Arrays.asList(mock(ViewManager.class)); + UIManagerModule uiManagerModule = new UIManagerModule( + mReactContext, + viewManagers, + mUIImplementation); + Map constants = uiManagerModule.getConstants(); + assertThat(constants) + .containsKey(CUSTOM_BUBBLING_EVENT_TYPES) + .containsKey(CUSTOM_DIRECT_EVENT_TYPES) + .containsKey("Dimensions"); + } + + @Test + public void testCustomBubblingEvents() { + ViewManager mockViewManager = mock(ViewManager.class); + List viewManagers = Arrays.asList(mockViewManager); + when(mockViewManager.getExportedCustomBubblingEventTypeConstants()) + .thenReturn(MapBuilder.of("onTwirl", TWIRL_BUBBLING_EVENT_MAP)); + UIManagerModule uiManagerModule = new UIManagerModule( + mReactContext, + viewManagers, + mUIImplementation); + Map constants = uiManagerModule.getConstants(); + assertThat((Map) constants.get(CUSTOM_BUBBLING_EVENT_TYPES)) + .contains(MapEntry.entry("onTwirl", TWIRL_BUBBLING_EVENT_MAP)) + .containsKey("topChange"); + } + + @Test + public void testCustomDirectEvents() { + ViewManager mockViewManager = mock(ViewManager.class); + List viewManagers = Arrays.asList(mockViewManager); + when(mockViewManager.getExportedCustomDirectEventTypeConstants()) + .thenReturn(MapBuilder.of("onTwirl", TWIRL_DIRECT_EVENT_MAP)); + UIManagerModule uiManagerModule = new UIManagerModule( + mReactContext, + viewManagers, + mUIImplementation); + Map constants = uiManagerModule.getConstants(); + assertThat((Map) constants.get(CUSTOM_DIRECT_EVENT_TYPES)) + .contains(MapEntry.entry("onTwirl", TWIRL_DIRECT_EVENT_MAP)) + .containsKey("topLoadingStart"); + } + + @Test + public void testCustomViewConstants() { + ViewManager mockViewManager = mock(ViewManager.class); + List viewManagers = Arrays.asList(mockViewManager); + when(mockViewManager.getName()).thenReturn("RedPandaPhotoOfTheDayView"); + when(mockViewManager.getExportedViewConstants()) + .thenReturn(MapBuilder.of("PhotoSizeType", MapBuilder.of("Small", 1, "Large", 2))); + UIManagerModule uiManagerModule = new UIManagerModule( + mReactContext, + viewManagers, + mUIImplementation); + Map constants = uiManagerModule.getConstants(); + assertThat(constants).containsKey("RedPandaPhotoOfTheDayView"); + assertThat((Map) constants.get("RedPandaPhotoOfTheDayView")).containsKey("Constants"); + assertThat((Map) valueAtPath(constants, "RedPandaPhotoOfTheDayView", "Constants")) + .containsKey("PhotoSizeType"); + } + + @Test + public void testNativeProps() { + ViewManager mockViewManager = mock(ViewManager.class); + List viewManagers = Arrays.asList(mockViewManager); + when(mockViewManager.getName()).thenReturn("SomeView"); + when(mockViewManager.getNativeProps()) + .thenReturn(MapBuilder.of("fooProp", "number")); + UIManagerModule uiManagerModule = new UIManagerModule( + mReactContext, + viewManagers, + mUIImplementation); + Map constants = uiManagerModule.getConstants(); + assertThat((String) valueAtPath(constants, "SomeView", "NativeProps", "fooProp")) + .isEqualTo("number"); + } + + @Test + public void testMergeConstants() { + ViewManager managerX = mock(ViewManager.class); + when(managerX.getExportedCustomDirectEventTypeConstants()).thenReturn(MapBuilder.of( + "onTwirl", + MapBuilder.of( + "registrationName", + "onTwirl", + "keyToOverride", + "valueX", + "mapToMerge", + MapBuilder.of("keyToOverride", "innerValueX", "anotherKey", "valueX")))); + + ViewManager managerY = mock(ViewManager.class); + when(managerY.getExportedCustomDirectEventTypeConstants()).thenReturn(MapBuilder.of( + "onTwirl", + MapBuilder.of( + "extraKey", + "extraValue", + "keyToOverride", + "valueY", + "mapToMerge", + MapBuilder.of("keyToOverride", "innerValueY", "extraKey", "valueY")))); + + List viewManagers = Arrays.asList(managerX, managerY); + UIManagerModule uiManagerModule = new UIManagerModule( + mReactContext, + viewManagers, + mUIImplementation); + Map constants = uiManagerModule.getConstants(); + assertThat((Map) constants.get(CUSTOM_DIRECT_EVENT_TYPES)).containsKey("onTwirl"); + + Map twirlMap = (Map) valueAtPath(constants, CUSTOM_DIRECT_EVENT_TYPES, "onTwirl"); + assertThat(twirlMap) + .contains(MapEntry.entry("registrationName", "onTwirl")) + .contains(MapEntry.entry("keyToOverride", "valueY")) + .contains(MapEntry.entry("extraKey", "extraValue")) + .containsKey("mapToMerge"); + + Map mapToMerge = (Map) valueAtPath(twirlMap, "mapToMerge"); + assertThat(mapToMerge) + .contains(MapEntry.entry("keyToOverride", "innerValueY")) + .contains(MapEntry.entry("anotherKey", "valueX")) + .contains(MapEntry.entry("extraKey", "valueY")); + } + + private static Object valueAtPath(Map nestedMap, String... keyPath) { + assertThat(keyPath).isNotEmpty(); + Object value = nestedMap; + for (String key : keyPath) { + assertThat(value).isInstanceOf(Map.class); + nestedMap = (Map) value; + assertThat(nestedMap).containsKey(key); + value = nestedMap.get(key); + } + return value; + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleTest.java new file mode 100644 index 000000000..26db08587 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/UIManagerModuleTest.java @@ -0,0 +1,827 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.uimanager; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.graphics.Color; +import android.view.Choreographer; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.facebook.react.ReactRootView; +import com.facebook.react.animation.Animation; +import com.facebook.react.animation.AnimationPropertyUpdater; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.SimpleArray; +import com.facebook.react.bridge.SimpleMap; +import com.facebook.react.views.text.ReactRawTextManager; +import com.facebook.react.views.text.ReactTextShadowNode; +import com.facebook.react.views.text.ReactTextViewManager; +import com.facebook.react.views.view.ReactViewGroup; +import com.facebook.react.views.view.ReactViewManager; +import com.facebook.react.bridge.ReactTestHelper; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +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.when; + +/** + * Tests for {@link UIManagerModule}. + */ +@PrepareForTest({Arguments.class, ReactChoreographer.class}) +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class UIManagerModuleTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private ReactApplicationContext mReactContext; + private CatalystInstance mCatalystInstanceMock; + private ArrayList mPendingChoreographerCallbacks; + + @Before + public void setUp() { + PowerMockito.mockStatic(Arguments.class, ReactChoreographer.class); + + ReactChoreographer choreographerMock = mock(ReactChoreographer.class); + PowerMockito.when(Arguments.createArray()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new SimpleArray(); + } + }); + PowerMockito.when(Arguments.createMap()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new SimpleMap(); + } + }); + PowerMockito.when(ReactChoreographer.getInstance()).thenReturn(choreographerMock); + + mPendingChoreographerCallbacks = new ArrayList<>(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + mPendingChoreographerCallbacks + .add((Choreographer.FrameCallback) invocation.getArguments()[1]); + return null; + } + }).when(choreographerMock).postFrameCallback( + any(ReactChoreographer.CallbackType.class), + any(Choreographer.FrameCallback.class)); + + mCatalystInstanceMock = ReactTestHelper.createMockCatalystInstance(); + mReactContext = new ReactApplicationContext(RuntimeEnvironment.application); + mReactContext.initializeWithInstance(mCatalystInstanceMock); + + UIManagerModule uiManagerModuleMock = mock(UIManagerModule.class); + when(mCatalystInstanceMock.getNativeModule(UIManagerModule.class)) + .thenReturn(uiManagerModuleMock); + } + + @Test + public void testCreateSimpleHierarchy() { + UIManagerModule uiManager = getUIManagerModule(); + + ViewGroup rootView = createSimpleTextHierarchy(uiManager, "Some text"); + + assertThat(rootView.getChildCount()).isEqualTo(1); + + View firstChild = rootView.getChildAt(0); + assertThat(firstChild).isInstanceOf(TextView.class); + assertThat(((TextView) firstChild).getText().toString()).isEqualTo("Some text"); + } + + @Test + public void testUpdateSimpleHierarchy() { + UIManagerModule uiManager = getUIManagerModule(); + + ViewGroup rootView = createSimpleTextHierarchy(uiManager, "Some text"); + TextView textView = (TextView) rootView.getChildAt(0); + + int rawTextTag = 3; + uiManager.updateView( + rawTextTag, + ReactRawTextManager.REACT_CLASS, + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "New text")); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertThat(textView.getText().toString()).isEqualTo("New text"); + } + + @Test + public void testHierarchyWithView() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = + new ReactRootView(RuntimeEnvironment.application.getApplicationContext()); + int rootTag = uiManager.addMeasuredRootView(rootView); + int viewTag = rootTag + 1; + int subViewTag = viewTag + 1; + + uiManager.createView( + viewTag, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + uiManager.createView( + subViewTag, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + + uiManager.manageChildren( + viewTag, + null, + null, + SimpleArray.of(subViewTag), + SimpleArray.of(0), + null); + + uiManager.manageChildren( + rootTag, + null, + null, + SimpleArray.of(viewTag), + SimpleArray.of(0), + null); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertThat(rootView.getChildCount()).isEqualTo(1); + + ViewGroup child = (ViewGroup) rootView.getChildAt(0); + assertThat(child.getChildCount()).isEqualTo(1); + + ViewGroup grandchild = (ViewGroup) child.getChildAt(0); + assertThat(grandchild).isInstanceOf(ViewGroup.class); + assertThat(grandchild.getChildCount()).isEqualTo(0); + } + + @Test + public void testMoveViews() { + UIManagerModule uiManager = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + View expectedViewAt0 = hierarchy.nativeRootView.getChildAt(1); + View expectedViewAt1 = hierarchy.nativeRootView.getChildAt(2); + View expectedViewAt2 = hierarchy.nativeRootView.getChildAt(0); + View expectedViewAt3 = hierarchy.nativeRootView.getChildAt(3); + + uiManager.manageChildren( + hierarchy.rootView, + SimpleArray.of(1, 0, 2), + SimpleArray.of(0, 2, 1), + null, + null, + null); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertChildrenAreExactly( + hierarchy.nativeRootView, + expectedViewAt0, + expectedViewAt1, + expectedViewAt2, + expectedViewAt3); + } + + @Test + public void testDeleteViews() { + UIManagerModule uiManager = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + View expectedViewAt0 = hierarchy.nativeRootView.getChildAt(1); + View expectedViewAt1 = hierarchy.nativeRootView.getChildAt(2); + + uiManager.manageChildren( + hierarchy.rootView, + null, + null, + null, + null, + SimpleArray.of(0, 3)); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertChildrenAreExactly( + hierarchy.nativeRootView, + expectedViewAt0, + expectedViewAt1); + } + + @Test + public void testMoveAndDeleteViews() { + UIManagerModule uiManager = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + View expectedViewAt0 = hierarchy.nativeRootView.getChildAt(0); + View expectedViewAt1 = hierarchy.nativeRootView.getChildAt(3); + View expectedViewAt2 = hierarchy.nativeRootView.getChildAt(2); + + uiManager.manageChildren( + hierarchy.rootView, + SimpleArray.of(3), + SimpleArray.of(1), + null, + null, + SimpleArray.of(1)); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertChildrenAreExactly( + hierarchy.nativeRootView, + expectedViewAt0, + expectedViewAt1, + expectedViewAt2); + } + + @Test(expected = IllegalViewOperationException.class) + public void testMoveAndDeleteRemoveViewsDuplicateRemove() { + UIManagerModule uiManager = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + uiManager.manageChildren( + hierarchy.rootView, + SimpleArray.of(3), + SimpleArray.of(1), + null, + null, + SimpleArray.of(3)); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + } + + @Test(expected = IllegalViewOperationException.class) + public void testDuplicateRemove() { + UIManagerModule uiManager = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + uiManager.manageChildren( + hierarchy.rootView, + null, + null, + null, + null, + SimpleArray.of(3, 3)); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + } + + @Test + public void testMoveAndAddViews() { + UIManagerModule uiManager = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + int textViewTag = 1000; + uiManager.createView( + textViewTag, + ReactTextViewManager.REACT_CLASS, + hierarchy.rootView, + SimpleMap.of("collapsable", false)); + + View expectedViewAt0 = hierarchy.nativeRootView.getChildAt(0); + View expectedViewAt1 = hierarchy.nativeRootView.getChildAt(3); + View expectedViewAt3 = hierarchy.nativeRootView.getChildAt(1); + View expectedViewAt4 = hierarchy.nativeRootView.getChildAt(2); + + uiManager.manageChildren( + hierarchy.rootView, + SimpleArray.of(1, 2, 3), + SimpleArray.of(3, 4, 1), + SimpleArray.of(textViewTag), + SimpleArray.of(2), + null); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertThat(hierarchy.nativeRootView.getChildCount()).isEqualTo(5); + assertThat(hierarchy.nativeRootView.getChildAt(0)).isEqualTo(expectedViewAt0); + assertThat(hierarchy.nativeRootView.getChildAt(1)).isEqualTo(expectedViewAt1); + assertThat(hierarchy.nativeRootView.getChildAt(3)).isEqualTo(expectedViewAt3); + assertThat(hierarchy.nativeRootView.getChildAt(4)).isEqualTo(expectedViewAt4); + } + + @Test + public void testMoveViewsWithChildren() { + UIManagerModule uiManager = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + View expectedViewAt0 = hierarchy.nativeRootView.getChildAt(0); + View expectedViewAt1 = hierarchy.nativeRootView.getChildAt(2); + View expectedViewAt2 = hierarchy.nativeRootView.getChildAt(1); + View expectedViewAt3 = hierarchy.nativeRootView.getChildAt(3); + + uiManager.manageChildren( + hierarchy.rootView, + SimpleArray.of(1, 2), + SimpleArray.of(2, 1), + null, + null, + null); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertChildrenAreExactly( + hierarchy.nativeRootView, + expectedViewAt0, + expectedViewAt1, + expectedViewAt2, + expectedViewAt3); + assertThat(((ViewGroup) hierarchy.nativeRootView.getChildAt(2)).getChildCount()).isEqualTo(2); + } + + @Test + public void testDeleteViewsWithChildren() { + UIManagerModule uiManager = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + View expectedViewAt0 = hierarchy.nativeRootView.getChildAt(0); + View expectedViewAt1 = hierarchy.nativeRootView.getChildAt(2); + View expectedViewAt2 = hierarchy.nativeRootView.getChildAt(3); + + uiManager.manageChildren( + hierarchy.rootView, + null, + null, + null, + null, + SimpleArray.of(1)); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertChildrenAreExactly( + hierarchy.nativeRootView, + expectedViewAt0, + expectedViewAt1, + expectedViewAt2); + } + + @Test + public void testLayoutAppliedToNodes() throws Exception { + UIManagerModule uiManager = getUIManagerModule(); + + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + int newViewTag = 10000; + uiManager.createView( + newViewTag, + ReactViewManager.REACT_CLASS, + hierarchy.rootView, + SimpleMap + .of("left", 10.0, "top", 20.0, "width", 30.0, "height", 40.0, "collapsable", false)); + + uiManager.manageChildren( + hierarchy.rootView, + null, + null, + SimpleArray.of(newViewTag), + SimpleArray.of(4), + null); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + View newView = hierarchy.nativeRootView.getChildAt(4); + assertThat(newView.getLeft()).isEqualTo(10); + assertThat(newView.getTop()).isEqualTo(20); + + assertThat(newView.getWidth()).isEqualTo(30); + assertThat(newView.getHeight()).isEqualTo(40); + } + + /** + * This is to make sure we execute enqueued operations in the order given by JS. + */ + @Test + public void testAddUpdateRemoveInSingleBatch() { + UIManagerModule uiManager = getUIManagerModule(); + + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + int newViewTag = 10000; + uiManager.createView( + newViewTag, + ReactViewManager.REACT_CLASS, + hierarchy.rootView, + SimpleMap.of("collapsable", false)); + + uiManager.manageChildren( + hierarchy.rootView, + null, + null, + SimpleArray.of(newViewTag), + SimpleArray.of(4), + null); + + uiManager.updateView( + newViewTag, + ReactViewManager.REACT_CLASS, + SimpleMap.of("backgroundColor", Color.RED)); + + uiManager.manageChildren( + hierarchy.rootView, + null, + null, + null, + null, + SimpleArray.of(4)); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertThat(hierarchy.nativeRootView.getChildCount()).isEqualTo(4); + } + + @Test + public void testTagsAssignment() { + UIManagerModule uiManager = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + View view0 = hierarchy.nativeRootView.getChildAt(0); + assertThat(view0.getId()).isEqualTo(hierarchy.view0); + + View viewWithChildren1 = hierarchy.nativeRootView.getChildAt(1); + assertThat(viewWithChildren1.getId()).isEqualTo(hierarchy.viewWithChildren1); + + View childView0 = ((ViewGroup) viewWithChildren1).getChildAt(0); + assertThat(childView0.getId()).isEqualTo(hierarchy.childView0); + + View childView1 = ((ViewGroup) viewWithChildren1).getChildAt(1); + assertThat(childView1.getId()).isEqualTo(hierarchy.childView1); + + View view2 = hierarchy.nativeRootView.getChildAt(2); + assertThat(view2.getId()).isEqualTo(hierarchy.view2); + + View view3 = hierarchy.nativeRootView.getChildAt(3); + assertThat(view3.getId()).isEqualTo(hierarchy.view3); + } + + @Test + public void testLayoutPropertyUpdatingOnlyOnLayoutChange() { + UIManagerModule uiManager = getUIManagerModule(); + + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + // Update layout to some values, this way we can verify it hasn't been updated, because the + // update process would normally reset it back to some non-negative value + View view0 = hierarchy.nativeRootView.getChildAt(0); + view0.layout(1, 2, 3, 4); + + // verify that X get updated when we update layout properties + uiManager.updateView( + hierarchy.view0, + ReactViewManager.REACT_CLASS, + SimpleMap.of("left", 10.0, "top", 20.0, "width", 30.0, "height", 40.0)); + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + assertThat(view0.getLeft()).isGreaterThan(2); + + // verify that the layout doesn't get updated when we update style property not affecting the + // position (e.g., background-color) + view0.layout(1, 2, 3, 4); + uiManager.updateView( + hierarchy.view0, + ReactViewManager.REACT_CLASS, + SimpleMap.of("backgroundColor", Color.RED)); + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + assertThat(view0.getLeft()).isEqualTo(1); + } + + private static class AnimationStub extends Animation { + + public AnimationStub(int animationID, AnimationPropertyUpdater propertyUpdater) { + super(animationID, propertyUpdater); + } + + @Override + public void run() { + } + } + + @Test + public void testAddAndRemoveAnimation() { + UIManagerModule uiManagerModule = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManagerModule); + + AnimationPropertyUpdater mockPropertyUpdater = mock(AnimationPropertyUpdater.class); + Animation mockAnimation = spy(new AnimationStub(1000, mockPropertyUpdater)); + Callback callbackMock = mock(Callback.class); + + int rootTag = hierarchy.rootView; + uiManagerModule.createView( + hierarchy.rootView, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + + uiManagerModule.registerAnimation(mockAnimation); + uiManagerModule.addAnimation(hierarchy.rootView, 1000, callbackMock); + uiManagerModule.removeAnimation(hierarchy.rootView, 1000); + + uiManagerModule.onBatchComplete(); + executePendingChoreographerCallbacks(); + + verify(callbackMock, times(1)).invoke(false); + verify(mockAnimation).run(); + verify(mockAnimation).cancel(); + } + + /** + * Makes sure replaceExistingNonRootView by replacing a view with a new view that has a background + * color set. + */ + @Test + public void testReplaceExistingNonRootView() { + UIManagerModule uiManager = getUIManagerModule(); + TestMoveDeleteHierarchy hierarchy = createMoveDeleteHierarchy(uiManager); + + int newViewTag = 1234; + uiManager.createView( + newViewTag, + ReactViewManager.REACT_CLASS, + hierarchy.rootView, + SimpleMap.of("backgroundColor", Color.RED)); + + uiManager.replaceExistingNonRootView(hierarchy.view2, newViewTag); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertThat(hierarchy.nativeRootView.getChildCount()).isEqualTo(4); + assertThat(hierarchy.nativeRootView.getChildAt(2)).isInstanceOf(ReactViewGroup.class); + ReactViewGroup view = (ReactViewGroup) hierarchy.nativeRootView.getChildAt(2); + assertThat(view.getBackgroundColor()).isEqualTo(Color.RED); + } + + /** + * Verifies removeSubviewsFromContainerWithID works by adding subviews, removing them, and + * checking that the final number of children is correct. + */ + @Test + public void testRemoveSubviewsFromContainerWithID() { + UIManagerModule uiManager = getUIManagerModule(); + ReactRootView rootView = + new ReactRootView(RuntimeEnvironment.application.getApplicationContext()); + int rootTag = uiManager.addMeasuredRootView(rootView); + + final int containerTag = rootTag + 1; + final int containerSiblingTag = containerTag + 1; + + uiManager.createView( + containerTag, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + uiManager.createView( + containerSiblingTag, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + addChild(uiManager, rootTag, containerTag, 0); + addChild(uiManager, rootTag, containerSiblingTag, 1); + + uiManager.createView( + containerTag + 2, + ReactTextViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + uiManager.createView( + containerTag + 3, + ReactTextViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + addChild(uiManager, containerTag, containerTag + 2, 0); + addChild(uiManager, containerTag, containerTag + 3, 1); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertThat(rootView.getChildCount()).isEqualTo(2); + assertThat(((ViewGroup)rootView.getChildAt(0)).getChildCount()).isEqualTo(2); + + uiManager.removeSubviewsFromContainerWithID(containerTag); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + assertThat(rootView.getChildCount()).isEqualTo(2); + assertThat(((ViewGroup)rootView.getChildAt(0)).getChildCount()).isEqualTo(0); + } + + /** + * Assuming no other views have been created, the root view will have tag 1, Text tag 2, and + * RawText tag 3. + */ + private ViewGroup createSimpleTextHierarchy(UIManagerModule uiManager, String text) { + ReactRootView rootView = + new ReactRootView(RuntimeEnvironment.application.getApplicationContext()); + int rootTag = uiManager.addMeasuredRootView(rootView); + int textTag = rootTag + 1; + int rawTextTag = textTag + 1; + + uiManager.createView( + textTag, + ReactTextViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + uiManager.createView( + rawTextTag, + ReactRawTextManager.REACT_CLASS, + rootTag, + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, text, "collapsable", false)); + + uiManager.manageChildren( + textTag, + null, + null, + SimpleArray.of(rawTextTag), + SimpleArray.of(0), + null); + + uiManager.manageChildren( + rootTag, + null, + null, + SimpleArray.of(textTag), + SimpleArray.of(0), + null); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + return rootView; + } + + private TestMoveDeleteHierarchy createMoveDeleteHierarchy(UIManagerModule uiManager) { + ReactRootView rootView = new ReactRootView(mReactContext); + int rootTag = uiManager.addMeasuredRootView(rootView); + + TestMoveDeleteHierarchy hierarchy = new TestMoveDeleteHierarchy(rootView, rootTag); + + uiManager.createView( + hierarchy.view0, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + uiManager.createView( + hierarchy.viewWithChildren1, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + uiManager.createView( + hierarchy.view2, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + uiManager.createView( + hierarchy.view3, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + uiManager.createView( + hierarchy.childView0, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + uiManager.createView( + hierarchy.childView1, + ReactViewManager.REACT_CLASS, + rootTag, + SimpleMap.of("collapsable", false)); + + addChild(uiManager, rootTag, hierarchy.view0, 0); + addChild(uiManager, rootTag, hierarchy.viewWithChildren1, 1); + addChild(uiManager, rootTag, hierarchy.view2, 2); + addChild(uiManager, rootTag, hierarchy.view3, 3); + + addChild(uiManager, hierarchy.viewWithChildren1, hierarchy.childView0, 0); + addChild(uiManager, hierarchy.viewWithChildren1, hierarchy.childView1, 1); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + return hierarchy; + } + + private void addChild(UIManagerModule uiManager, int parentTag, int childTag, int index) { + uiManager.manageChildren( + parentTag, + null, + null, + SimpleArray.of(childTag), + SimpleArray.of(index), + null); + } + + private void assertChildrenAreExactly(ViewGroup parent, View... views) { + assertThat(parent.getChildCount()).isEqualTo(views.length); + for (int i = 0; i < views.length; i++) { + assertThat(parent.getChildAt(i)) + .describedAs("View at " + i) + .isEqualTo(views[i]); + } + } + + /** + * Holder for the tags that represent that represent views in the following hierarchy: + * - View rootView + * - View view0 + * - View viewWithChildren1 + * - View childView0 + * - View childView1 + * - View view2 + * - View view3 + * + * This hierarchy is used to test move/delete functionality in manageChildren. + */ + private static class TestMoveDeleteHierarchy { + + public ReactRootView nativeRootView; + public int rootView; + public int view0; + public int viewWithChildren1; + public int view2; + public int view3; + public int childView0; + public int childView1; + + public TestMoveDeleteHierarchy(ReactRootView nativeRootView, int rootViewTag) { + this.nativeRootView = nativeRootView; + rootView = rootViewTag; + view0 = rootView + 1; + viewWithChildren1 = rootView + 2; + view2 = rootView + 3; + view3 = rootView + 4; + childView0 = rootView + 5; + childView1 = rootView + 6; + } + } + + private void executePendingChoreographerCallbacks() { + ArrayList callbacks = + new ArrayList<>(mPendingChoreographerCallbacks); + mPendingChoreographerCallbacks.clear(); + for (Choreographer.FrameCallback frameCallback : callbacks) { + frameCallback.doFrame(0); + } + } + + private UIManagerModule getUIManagerModule() { + List viewManagers = Arrays.asList( + new ReactViewManager(), + new ReactTextViewManager(), + new ReactRawTextManager()); + UIManagerModule uiManagerModule = new UIManagerModule( + mReactContext, + viewManagers, + new UIImplementation(mReactContext, viewManagers)); + uiManagerModule.onHostResume(); + return uiManagerModule; + } +} diff --git a/circle.yml b/circle.yml index eff3e3405..841ebcb5a 100644 --- a/circle.yml +++ b/circle.yml @@ -17,17 +17,17 @@ dependencies: - if [[ ! -e buck ]]; then git clone https://github.com/facebook/buck.git; fi - cd buck && ant - buck/bin/buck --version - - buck/bin/buck fetch ReactAndroid/src/test/java/com/facebook/react/modules - - buck/bin/buck fetch ReactAndroid/src/main/java/com/facebook/react - - buck/bin/buck fetch ReactAndroid/src/main/java/com/facebook/react/shell - - buck/bin/buck fetch ReactAndroid/src/androidTest/java/com/facebook/react/tests + - buck/bin/buck fetch ReactAndroid/src/main/... + - buck/bin/buck fetch ReactAndroid/src/test/... + - buck/bin/buck fetch ReactAndroid/src/androidTest/... # using npm@3 because of problems with shrink-wrapped optional deps installs on linux - npm install -g npm@3.2 - source scripts/circle-ci-android-setup.sh && getAndroidSDK - ./gradlew :ReactAndroid:downloadBoost :ReactAndroid:downloadDoubleConversion :ReactAndroid:downloadFolly :ReactAndroid:downloadGlog cache_directories: - "ReactAndroid/build/downloads" - - "buck-out" + - "buck" + - "buck-out/bin" test: pre: # starting emulator in advance because it takes very long to boot @@ -39,7 +39,7 @@ test: - source scripts/circle-ci-android-setup.sh && waitForAVD override: # buck tests - - buck/bin/buck test ReactAndroid/src/test/java/com/facebook/react/modules + - buck/bin/buck test ReactAndroid/src/test/... - buck/bin/buck build ReactAndroid/src/main/java/com/facebook/react - buck/bin/buck build ReactAndroid/src/main/java/com/facebook/react/shell # temp, we can't run instrumentation tests yet