diff --git a/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK b/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK index 9c618db04..7b410d076 100644 --- a/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK +++ b/ReactAndroid/src/main/libraries/fresco/fresco-react-native/BUCK @@ -22,8 +22,18 @@ remote_file( sha1 = 'a944015ddf50fdad79302e42a85a351633c24472', ) +android_library( + name = 'imagepipeline', + exported_deps = [ + ':imagepipeline-core', + ':bolts', + ], + visibility = ['//ReactAndroid/...',], +) + + android_prebuilt_aar( - name = 'imagepipeline', + name = 'imagepipeline-core', aar = ':imagepipeline-aar', visibility = ['//ReactAndroid/...',], ) @@ -34,6 +44,18 @@ remote_file( sha1 = '93fe3e629c03aea8f63dabd80a0e616b0caef65b', ) +prebuilt_jar( + name = 'bolts', + binary_jar = ':download-bolts', + visibility = ['//ReactAndroid/...',], +) + +remote_file( + name = 'download-bolts', + url = 'mvn:com.parse.bolts:bolts-tasks:jar:1.4.0', + sha1 = 'd85884acf6810a3bbbecb587f239005cbc846dc4', +) + android_prebuilt_aar( name = 'fbcore', aar = ':fbcore-aar', diff --git a/ReactAndroid/src/main/third-party/java/jackson/BUCK b/ReactAndroid/src/main/third-party/java/jackson/BUCK index 818c52c8a..e6377adf4 100644 --- a/ReactAndroid/src/main/third-party/java/jackson/BUCK +++ b/ReactAndroid/src/main/third-party/java/jackson/BUCK @@ -1,3 +1,14 @@ +include_defs('//ReactAndroid/DEFS') + +android_library( + name = 'jackson', + exported_deps = [ + ':databind', + ':annotations', + ], + visibility = ['//ReactAndroid/...',], +) + prebuilt_jar( name = 'core', binary_jar = ':jackson-core-binary-jar', @@ -9,3 +20,27 @@ remote_file( url = 'mvn:com.fasterxml.jackson.core:jackson-core:jar:2.2.3', sha1 = '1a0113da2cab5f4c216b4e5e7c1dbfaa67087e14', ) + +prebuilt_jar( + name = 'databind', + binary_jar = ':jackson-databind-jar', + visibility = ['//ReactAndroid/...',], +) + +remote_file( + name = 'jackson-databind-jar', + url = 'mvn:com.fasterxml.jackson.core:jackson-databind:jar:2.2.3', + sha1 = '03ae380888029daefb91d3ecdca3a37d8cb92bc9', +) + +prebuilt_jar( + name = 'annotations', + binary_jar = ':jackson-annotations-jar', + visibility = ['//ReactAndroid/...',], +) + +remote_file( + name = 'jackson-annotations-jar', + url = 'mvn:com.fasterxml.jackson.core:jackson-annotations:jar:2.2.3', + sha1 = '0527fece4f23a457070a36c371a26d6c0208e1c3', +) diff --git a/ReactAndroid/src/test/java/com/facebook/react/BUCK b/ReactAndroid/src/test/java/com/facebook/react/BUCK new file mode 100644 index 000000000..0396f42ae --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/BUCK @@ -0,0 +1,34 @@ +include_defs('//ReactAndroid/DEFS') + +robolectric3_test( + name = 'react', + # 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/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('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'), + react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), + ], +) + +project_config( + test_target = ':react', +) + diff --git a/ReactAndroid/src/test/java/com/facebook/react/CompositeReactPackageTest.java b/ReactAndroid/src/test/java/com/facebook/react/CompositeReactPackageTest.java new file mode 100644 index 000000000..38e2dce48 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/CompositeReactPackageTest.java @@ -0,0 +1,218 @@ +/** + * 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; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.Rule; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class CompositeReactPackageTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Mock ReactPackage packageNo1; + @Mock ReactPackage packageNo2; + @Mock ReactPackage packageNo3; + + @Mock ReactApplicationContext reactContext; + + @Before + public void initMocks() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testThatCreateNativeModulesIsCalledOnAllPackages() { + // Given + CompositeReactPackage composite = new CompositeReactPackage(packageNo1, packageNo2, packageNo3); + + // When + composite.createNativeModules(reactContext); + + // Then + verify(packageNo1).createNativeModules(reactContext); + verify(packageNo2).createNativeModules(reactContext); + verify(packageNo3).createNativeModules(reactContext); + } + + @Test + public void testThatCreateJSModulesIsCalledOnAllPackages() { + // Given + CompositeReactPackage composite = new CompositeReactPackage(packageNo1, packageNo2, packageNo3); + + // When + composite.createJSModules(); + + // Then + verify(packageNo1).createJSModules(); + verify(packageNo2).createJSModules(); + verify(packageNo3).createJSModules(); + } + + @Test + public void testThatCreateViewManagersIsCalledOnAllPackages() { + // Given + CompositeReactPackage composite = new CompositeReactPackage(packageNo1, packageNo2, packageNo3); + + // When + composite.createViewManagers(reactContext); + + // Then + verify(packageNo1).createViewManagers(reactContext); + verify(packageNo2).createViewManagers(reactContext); + verify(packageNo3).createViewManagers(reactContext); + } + + @Test + public void testThatCompositeReturnsASumOfNativeModules() { + // Given + CompositeReactPackage composite = new CompositeReactPackage(packageNo1, packageNo2); + + NativeModule moduleNo1 = mock(NativeModule.class); + when(moduleNo1.getName()).thenReturn("ModuleNo1"); + + // module2 and module3 will share same name, composite should return only the latter one + final String sameModuleName = "SameModuleName"; + + NativeModule moduleNo2 = mock(NativeModule.class); + when(moduleNo2.getName()).thenReturn(sameModuleName); + + NativeModule moduleNo3 = mock(NativeModule.class); + when(moduleNo3.getName()).thenReturn(sameModuleName); + + NativeModule moduleNo4 = mock(NativeModule.class); + when(moduleNo4.getName()).thenReturn("ModuleNo4"); + + when(packageNo1.createNativeModules(reactContext)).thenReturn( + Arrays.asList(new NativeModule[]{moduleNo1, moduleNo2})); + + when(packageNo2.createNativeModules(reactContext)).thenReturn( + Arrays.asList(new NativeModule[]{moduleNo3, moduleNo4})); + + // When + List compositeModules = composite.createNativeModules(reactContext); + + // Then + + // Wrapping lists into sets to be order-independent. + // Note that there should be no module2 returned. + Set expected = new HashSet<>( + Arrays.asList(new NativeModule[]{moduleNo1, moduleNo3, moduleNo4})); + Set actual = new HashSet<>(compositeModules); + + assertEquals(expected, actual); + } + + @Test + public void testThatCompositeReturnsASumOfViewManagers() { + // Given + CompositeReactPackage composite = new CompositeReactPackage(packageNo1, packageNo2); + + ViewManager managerNo1 = mock(ViewManager.class); + when(managerNo1.getName()).thenReturn("ManagerNo1"); + + // managerNo2 and managerNo3 will share same name, composite should return only the latter one + final String sameModuleName = "SameModuleName"; + + ViewManager managerNo2 = mock(ViewManager.class); + when(managerNo2.getName()).thenReturn(sameModuleName); + + ViewManager managerNo3 = mock(ViewManager.class); + when(managerNo3.getName()).thenReturn(sameModuleName); + + ViewManager managerNo4 = mock(ViewManager.class); + when(managerNo4.getName()).thenReturn("ManagerNo4"); + + when(packageNo1.createViewManagers(reactContext)).thenReturn( + Arrays.asList(new ViewManager[]{managerNo1, managerNo2})); + + when(packageNo2.createViewManagers(reactContext)).thenReturn( + Arrays.asList(new ViewManager[]{managerNo3, managerNo4})); + + // When + List compositeModules = composite.createViewManagers(reactContext); + + // Then + + // Wrapping lists into sets to be order-independent. + // Note that there should be no managerNo2 returned. + Set expected = new HashSet<>( + Arrays.asList(new ViewManager[]{managerNo1, managerNo3, managerNo4}) + ); + Set actual = new HashSet<>(compositeModules); + + assertEquals(expected, actual); + } + + // public access level is required by Mockito + public static class JavaScriptModuleNo1 implements JavaScriptModule {}; + public static class JavaScriptModuleNo2 implements JavaScriptModule {}; + public static class JavaScriptModuleNo3 implements JavaScriptModule {}; + + @Test + public void testThatCompositeReturnsASumOfJSModules() { + // Given + CompositeReactPackage composite = new CompositeReactPackage(packageNo1, packageNo2); + + Class moduleNo1 = mock(JavaScriptModuleNo1.class).getClass(); + Class moduleNo2 = mock(JavaScriptModuleNo2.class).getClass(); + Class moduleNo3 = mock(JavaScriptModuleNo3.class).getClass(); + + List> l1 = new ArrayList<>(); + l1.add(moduleNo1); + when(packageNo1.createJSModules()).thenReturn(l1); + + List> l2 = new ArrayList<>(); + l2.add(moduleNo2); + l2.add(moduleNo3); + when(packageNo2.createJSModules()).thenReturn(l2); + + // When + List> compositeModules = composite.createJSModules(); + + // Then + + // wrapping lists into sets to be order-independent + List> l3 = new ArrayList<>(); + l3.add(moduleNo1); + l3.add(moduleNo2); + l3.add(moduleNo3); + Set> expected = new HashSet<>(l3); + Set> actual = new HashSet<>(compositeModules); + + assertEquals(expected, actual); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/RootViewTest.java b/ReactAndroid/src/test/java/com/facebook/react/RootViewTest.java new file mode 100644 index 000000000..388d5064d --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/RootViewTest.java @@ -0,0 +1,192 @@ +/** + * 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; + +import java.util.Date; + +import android.util.DisplayMetrics; +import android.view.MotionEvent; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.ReactTestHelper; +import com.facebook.react.bridge.SimpleArray; +import com.facebook.react.bridge.SimpleMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.DisplayMetricsHolder; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.Rule; +import org.mockito.ArgumentCaptor; +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.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@PrepareForTest(Arguments.class) +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class RootViewTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private ReactContext mReactContext; + private CatalystInstance mCatalystInstanceMock; + + @Before + public void setUp() { + PowerMockito.mockStatic(Arguments.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(); + } + }); + + mCatalystInstanceMock = ReactTestHelper.createMockCatalystInstance(); + mReactContext = new ReactApplicationContext(RuntimeEnvironment.application); + mReactContext.initializeWithInstance(mCatalystInstanceMock); + DisplayMetrics displayMetrics = mReactContext.getResources().getDisplayMetrics(); + DisplayMetricsHolder.setDisplayMetrics(displayMetrics); + + UIManagerModule uiManagerModuleMock = mock(UIManagerModule.class); + when(mCatalystInstanceMock.getNativeModule(UIManagerModule.class)) + .thenReturn(uiManagerModuleMock); + } + + @Test + public void testTouchEmitter() { + ReactInstanceManager instanceManager = mock(ReactInstanceManager.class); + when(instanceManager.getCurrentReactContext()).thenReturn(mReactContext); + + UIManagerModule uiManager = mock(UIManagerModule.class); + EventDispatcher eventDispatcher = mock(EventDispatcher.class); + RCTEventEmitter eventEmitterModuleMock = mock(RCTEventEmitter.class); + when(mCatalystInstanceMock.getNativeModule(UIManagerModule.class)) + .thenReturn(uiManager); + when(uiManager.getEventDispatcher()).thenReturn(eventDispatcher); + + int rootViewId = 7; + + ReactRootView rootView = new ReactRootView(mReactContext); + rootView.setId(rootViewId); + rootView.startReactApplication(instanceManager, ""); + rootView.simulateAttachForTesting(); + + long ts = new Date().getTime(); + + // Test ACTION_DOWN event + rootView.onTouchEvent( + MotionEvent.obtain(100, ts, MotionEvent.ACTION_DOWN, 0, 0, 0)); + + ArgumentCaptor downEventCaptor = ArgumentCaptor.forClass(Event.class); + verify(eventDispatcher).dispatchEvent(downEventCaptor.capture()); + verifyNoMoreInteractions(eventDispatcher); + + downEventCaptor.getValue().dispatch(eventEmitterModuleMock); + + ArgumentCaptor downActionTouchesArgCaptor = + ArgumentCaptor.forClass(SimpleArray.class); + verify(eventEmitterModuleMock).receiveTouches( + eq("topTouchStart"), + downActionTouchesArgCaptor.capture(), + any(SimpleArray.class)); + verifyNoMoreInteractions(eventEmitterModuleMock); + + assertThat(downActionTouchesArgCaptor.getValue().size()).isEqualTo(1); + assertThat(downActionTouchesArgCaptor.getValue().getMap(0)).isEqualTo( + SimpleMap.of( + "pageX", + 0., + "pageY", + 0., + "locationX", + 0., + "locationY", + 0., + "target", + rootViewId, + "timeStamp", + (double) ts, + "identifier", + 0.)); + + // Test ACTION_UP event + reset(eventEmitterModuleMock, eventDispatcher); + + ArgumentCaptor upEventCaptor = ArgumentCaptor.forClass(Event.class); + ArgumentCaptor upActionTouchesArgCaptor = + ArgumentCaptor.forClass(SimpleArray.class); + + rootView.onTouchEvent( + MotionEvent.obtain(50, ts, MotionEvent.ACTION_UP, 0, 0, 0)); + verify(eventDispatcher).dispatchEvent(upEventCaptor.capture()); + verifyNoMoreInteractions(eventDispatcher); + + upEventCaptor.getValue().dispatch(eventEmitterModuleMock); + verify(eventEmitterModuleMock).receiveTouches( + eq("topTouchEnd"), + upActionTouchesArgCaptor.capture(), + any(WritableArray.class)); + verifyNoMoreInteractions(eventEmitterModuleMock); + + assertThat(upActionTouchesArgCaptor.getValue().size()).isEqualTo(1); + assertThat(upActionTouchesArgCaptor.getValue().getMap(0)).isEqualTo( + SimpleMap.of( + "pageX", + 0., + "pageY", + 0., + "locationX", + 0., + "locationY", + 0., + "target", + rootViewId, + "timeStamp", + (double) ts, + "identifier", + 0.)); + + // Test other action + reset(eventDispatcher); + rootView.onTouchEvent( + MotionEvent.obtain(50, new Date().getTime(), MotionEvent.ACTION_HOVER_MOVE, 0, 0, 0)); + verifyNoMoreInteractions(eventDispatcher); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/bridge/BUCK b/ReactAndroid/src/test/java/com/facebook/react/bridge/BUCK index bb67c85be..9e0251c78 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/bridge/BUCK +++ b/ReactAndroid/src/test/java/com/facebook/react/bridge/BUCK @@ -1,12 +1,12 @@ include_defs('//ReactAndroid/DEFS') STANDARD_TEST_SRCS = [ - '**/*Test.java', + '*Test.java', ] android_library( name = 'testhelpers', - srcs = glob(['**/*.java'], excludes = STANDARD_TEST_SRCS), + srcs = glob(['*.java'], excludes = STANDARD_TEST_SRCS), deps = [ react_native_target('java/com/facebook/react/bridge:bridge'), react_native_target('java/com/facebook/react/uimanager:uimanager'), @@ -18,3 +18,32 @@ android_library( 'PUBLIC' ], ) + +robolectric3_test( + name = 'bridge', + # Please change the contact to the oncall of your team + contacts = ['oncall+fbandroid_sheriff@xmail.facebook.com'], + srcs = glob(STANDARD_TEST_SRCS), + deps = [ + ':testhelpers', + 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/uimanager:uimanager'), + + 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/mockito:mockito'), + react_native_dep('third-party/java/jackson:core'), + react_native_dep('third-party/java/jackson:jackson'), + react_native_dep('third-party/java/jsr-305:jsr-305'), + react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), + ], + visibility = [ + 'PUBLIC' + ], +) + +project_config( + test_target = ':bridge', +) diff --git a/ReactAndroid/src/test/java/com/facebook/react/bridge/JavaScriptModuleConfigTest.java b/ReactAndroid/src/test/java/com/facebook/react/bridge/JavaScriptModuleConfigTest.java new file mode 100644 index 000000000..6b2b9ea7b --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/bridge/JavaScriptModuleConfigTest.java @@ -0,0 +1,98 @@ +/** + * 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.bridge; + +import java.io.IOException; +import java.io.StringWriter; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import static org.fest.assertions.api.Assertions.assertThat; + +public class JavaScriptModuleConfigTest { + + private static interface SomeModule extends JavaScriptModule { + public void stringMethod(String arg); + public void intMethod(int arg); + } + + private static interface OtherModule extends JavaScriptModule { + public void method(String arg1, int arg2); + } + + @Test + public void testModuleWithMethods() throws Exception { + JavaScriptModulesConfig jsModulesConfig = new JavaScriptModulesConfig.Builder() + .add(SomeModule.class) + .build(); + + String json = getModuleDescriptions(jsModulesConfig); + JsonNode node = parse(json); + assertThat(node).hasSize(1); + + JsonNode module = node.fields().next().getValue(); + assertThat(module).isNotNull(); + + JsonNode methods = module.get("methods"); + assertThat(methods) + .isNotNull() + .hasSize(2); + + JsonNode intMethodNode = methods.get("intMethod"); + assertThat(intMethodNode).isNotNull(); + assertThat(intMethodNode.get("methodID").asInt()).isEqualTo(0); + + JsonNode stringMethod = methods.get("stringMethod"); + assertThat(stringMethod).isNotNull(); + assertThat(stringMethod.get("methodID").asInt()).isEqualTo(1); + } + + @Test + public void testMultipleModules() throws Exception { + JavaScriptModulesConfig jsModulesConfig = new JavaScriptModulesConfig.Builder() + .add(OtherModule.class) + .add(SomeModule.class) + .build(); + + String json = getModuleDescriptions(jsModulesConfig); + JsonNode node = parse(json); + assertThat(node).hasSize(2); + + JsonNode someModuleNode = node.get("SomeModule"); + assertThat(someModuleNode).isNotNull(); + int someModuleID = someModuleNode.get("moduleID").asInt(); + + JsonNode otherModuleNode = node.get("OtherModule"); + assertThat(otherModuleNode).isNotNull(); + int otherModuleID = otherModuleNode.get("moduleID").asInt(); + assertThat(otherModuleID) + .isNotEqualTo(someModuleID); + } + + private static String getModuleDescriptions(JavaScriptModulesConfig jsModulesConfig) + throws IOException { + JsonFactory jsonFactory = new JsonFactory(); + StringWriter writer = new StringWriter(); + JsonGenerator jg = jsonFactory.createGenerator(writer); + jsModulesConfig.writeModuleDescriptions(jg); + jg.close(); + return writer.getBuffer().toString(); + } + + private JsonNode parse(String json) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readTree(json); + } + +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/bridge/NativeModuleRegistryTest.java b/ReactAndroid/src/test/java/com/facebook/react/bridge/NativeModuleRegistryTest.java new file mode 100644 index 000000000..813120c21 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/bridge/NativeModuleRegistryTest.java @@ -0,0 +1,296 @@ +/** + * 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.bridge; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import com.facebook.react.common.MapBuilder; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.TextNode; +import org.junit.Test; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.robolectric.RobolectricTestRunner; + +import static org.fest.assertions.api.Assertions.assertThat; + +/** + * Tests for {@link NativeModuleRegistry}. + */ +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class NativeModuleRegistryTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Test + public void testModuleWithMethods() throws Exception { + NativeModuleRegistry registry = new NativeModuleRegistry.Builder() + .add(new MethodsModule()) + .build(); + + String json = getModuleDescriptions(registry); + JsonNode node = parse(json); + + JsonNode module = node.fields().next().getValue(); + assertThat(module).isNotNull(); + + JsonNode methods = module.get("methods"); + assertThat(methods).isNotNull(); + + ArrayList expected = new ArrayList(); + expected.add("doSomething"); + expected.add("saveData"); + + assertMethodsContainExactly(methods, expected); + } + + @Test + public void testAsyncMethod() throws Exception { + NativeModuleRegistry registry = new NativeModuleRegistry.Builder() + .add(new MethodsModule()) + .build(); + + String json = getModuleDescriptions(registry); + JsonNode node = parse(json); + + JsonNode asyncMethodData = node.get("TestModule").get("methods").get("saveData"); + assertThat(asyncMethodData.get("type")).isEqualTo(new TextNode("remoteAsync")); + + JsonNode regularMethodData = node.get("TestModule").get("methods").get("doSomething"); + assertThat(regularMethodData.get("type")).isNotEqualTo(new TextNode("remoteAsync")); + } + + @Test + public void testModuleWithConstants() throws Exception { + ConstantsModule constantsModule = new ConstantsModule(); + NativeModuleRegistry registry = new NativeModuleRegistry.Builder() + .add(constantsModule) + .build(); + + String json = getModuleDescriptions(registry); + JsonNode node = parse(json); + + JsonNode module = node.fields().next().getValue(); + assertThat(module).isNotNull(); + + JsonNode methods = module.get("methods"); + assertThat(methods).isNotNull(); + + ArrayList expected = new ArrayList(); + expected.add("runDMC"); + + assertMethodsContainExactly(methods, expected); + + JsonNode constants = module.get("constants"); + assertThat(constants.get("testInt").asInt()).isEqualTo(3); + assertThat(constants.get("testDouble").asDouble()).isEqualTo(3.14); + assertThat(constants.get("testString").asText()).isEqualTo("red panda"); + + JsonNode stringMap = constants.get("testStringMap"); + assertThat(stringMap.get("war_room").asText()).isEqualTo("17.1"); + assertThat(stringMap.get("android_corex").asText()).isEqualTo("16.1"); + + JsonNode intMap = constants.get("testIntMap"); + assertThat(intMap.get("42").asInt()).isEqualTo(1); + assertThat(intMap.get("84").asInt()).isEqualTo(2); + + JsonNode stringList = constants.get("testStringList"); + assertThat(stringList.get(0).asText()).isEqualTo("vulpes vulpes"); + assertThat(stringList.get(4).asText()).isEqualTo("vulpes velox"); + + JsonNode intList = constants.get("testIntList"); + assertThat(intList.get(0).asInt()).isEqualTo(3); + assertThat(intList.get(4).asInt()).isEqualTo(5); + } + + @Test + public void testModuleWithOnlyConstants() throws Exception { + OnlyConstantsModule onlyConstantsModule = new OnlyConstantsModule(); + NativeModuleRegistry registry = new NativeModuleRegistry.Builder() + .add(onlyConstantsModule) + .build(); + + String json = getModuleDescriptions(registry); + JsonNode node = parse(json); + + JsonNode module = node.fields().next().getValue(); + assertThat(module).isNotNull(); + + JsonNode constants = module.get("constants"); + assertThat(constants.get("testInt").asInt()).isEqualTo(4); + } + + @Test + public void testModuleWithNestedMapConstants() throws Exception { + NestedMapConstantsModule nestedMapConstantsModule = new NestedMapConstantsModule(); + NativeModuleRegistry registry = new NativeModuleRegistry.Builder() + .add(nestedMapConstantsModule) + .build(); + + String json = getModuleDescriptions(registry); + JsonNode node = parse(json); + + JsonNode module = node.fields().next().getValue(); + assertThat(module).isNotNull(); + + JsonNode constants = module.get("constants"); + assertThat(constants).isNotNull(); + + JsonNode nestedMapConstant = constants.get("nestedMap"); + assertThat(nestedMapConstant).isNotNull(); + + JsonNode firstLevel = nestedMapConstant.get("weNeedToGoDeeper"); + assertThat(firstLevel).isNotNull(); + + JsonNode secondLevel = firstLevel.get("evenDeeper"); + assertThat(secondLevel).isNotNull(); + + assertThat(secondLevel.get("inception").asBoolean()).isTrue(); + } + + private JsonNode parse(String json) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readTree(json); + } + + private void assertMethodsContainExactly(JsonNode methodsObject, List methodNames) { + ArrayList actual = new ArrayList(); + Iterator> fields = methodsObject.fields(); + while (fields.hasNext()) { + String name = fields.next().getKey(); + actual.add(name); + } + assertThat(actual) + .hasSize(methodNames.size()) + .containsAll(methodNames); + } + + private static String getModuleDescriptions(NativeModuleRegistry registry) + throws IOException { + JsonFactory jsonFactory = new JsonFactory(); + StringWriter writer = new StringWriter(); + JsonGenerator jg = jsonFactory.createGenerator(writer); + registry.writeModuleDescriptions(jg); + jg.close(); + return writer.getBuffer().toString(); + } + + private static class MethodsModule extends BaseJavaModule { + + @Override + public String getName() { + return "TestModule"; + } + + public void notACatalystMethod() { + } + + @ReactMethod + public void doSomething() { + } + + @ReactMethod + public void saveData(Promise promise) { + } + } + + private static class ConstantsModule extends BaseJavaModule { + + @Override + public String getName() { + return "ConstantsModule"; + } + + @Override + public Map getConstants() { + HashMap constants = new HashMap(); + constants.put("testInt", 3); + constants.put("testDouble", 3.14); + constants.put("testString", "red panda"); + constants.put( + "testStringMap", + MapBuilder.of( + "war_room", + "17.1", + "android_corex", + "16.1")); + constants.put( + "testIntMap", + MapBuilder.of( + 42, + 1, + 84, + 2)); + constants.put( + "testStringList", + Arrays.asList( + new String[]{ + "vulpes vulpes", + "vulpes cana", + "vulpes chama", + "vulpes fulfa", + "vulpes velox"})); + constants.put("testIntList", Arrays.asList(3, 1, 4, 1, 5)); + return constants; + } + + @ReactMethod + public void runDMC() { + } + } + + private static class OnlyConstantsModule extends BaseJavaModule { + + @Override + public String getName() { + return "OnlyConstantsModule"; + } + + @Override + public Map getConstants() { + return MapBuilder.of("testInt", 4); + } + } + + private static class NestedMapConstantsModule extends BaseJavaModule { + + @Override + public String getName() { + return "NestedMapConstantsModule"; + } + + @Override + public Map getConstants() { + return MapBuilder.of( + "nestedMap", + MapBuilder.of( + "weNeedToGoDeeper", + MapBuilder.of( + "evenDeeper", + MapBuilder.of("inception", true)))); + } + } + +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/BUCK b/ReactAndroid/src/test/java/com/facebook/react/views/BUCK new file mode 100644 index 000000000..5834b9374 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/views/BUCK @@ -0,0 +1,39 @@ +include_defs('//ReactAndroid/DEFS') + +robolectric3_test( + name = 'views', + # 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/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/image:image'), + react_native_target('java/com/facebook/react/views/text:text'), + react_native_target('java/com/facebook/react/views/textinput:textinput'), + react_native_target('java/com/facebook/react/views/view:view'), + + react_native_tests_target('java/com/facebook/react/bridge:testhelpers'), + 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'), + + react_native_dep('libraries/fbcore/src/test/java/com/facebook/powermock:powermock'), + react_native_dep('libraries/fresco/fresco-react-native:fresco-react-native'), + react_native_dep('libraries/fresco/fresco-react-native:imagepipeline'), + react_native_dep('libraries/fresco/fresco-react-native:fresco-drawee'), + ], +) + +project_config( + test_target = ':views', +) diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/image/ImageResizeModeTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/image/ImageResizeModeTest.java new file mode 100644 index 000000000..f74b4528e --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/views/image/ImageResizeModeTest.java @@ -0,0 +1,49 @@ +/** + * 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.views.image; + +import com.facebook.drawee.drawable.ScalingUtils; + +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.junit.Test; +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; + +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ImageResizeModeTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Test + public void testImageResizeMode() { + assertThat(ImageResizeMode.toScaleType(null)) + .isEqualTo(ScalingUtils.ScaleType.CENTER_CROP); + + assertThat(ImageResizeMode.toScaleType("contain")) + .isEqualTo(ScalingUtils.ScaleType.CENTER_INSIDE); + + assertThat(ImageResizeMode.toScaleType("cover")) + .isEqualTo(ScalingUtils.ScaleType.CENTER_CROP); + + assertThat(ImageResizeMode.toScaleType("stretch")) + .isEqualTo(ScalingUtils.ScaleType.FIT_XY); + + // No resizeMode set + assertThat(ImageResizeMode.defaultValue()) + .isEqualTo(ScalingUtils.ScaleType.CENTER_CROP); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.java new file mode 100644 index 000000000..030ce793e --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/views/image/ReactImagePropertyTest.java @@ -0,0 +1,134 @@ +/** + * 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.views.image; + +import android.graphics.Color; +import android.util.DisplayMetrics; + +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.ReactTestHelper; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.SimpleMap; +import com.facebook.react.uimanager.ReactStylesDiffMap; +import com.facebook.react.uimanager.DisplayMetricsHolder; +import com.facebook.react.uimanager.ThemedReactContext; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.Robolectric; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * Verify that {@link ScalingUtils} properties are being applied correctly + * by {@link ReactImageManager}. + */ +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ReactImagePropertyTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private ReactApplicationContext mContext; + private CatalystInstance mCatalystInstanceMock; + private ThemedReactContext mThemeContext; + + @Before + public void setup() { + mContext = new ReactApplicationContext(RuntimeEnvironment.application); + mCatalystInstanceMock = ReactTestHelper.createMockCatalystInstance(); + mContext.initializeWithInstance(mCatalystInstanceMock); + mThemeContext = new ThemedReactContext(mContext, mContext); + Fresco.initialize(mContext); + DisplayMetricsHolder.setDisplayMetrics(new DisplayMetrics()); + } + + @After + public void teardown() { + DisplayMetricsHolder.setDisplayMetrics(null); + } + + public ReactStylesDiffMap buildStyles(Object... keysAndValues) { + return new ReactStylesDiffMap(SimpleMap.of(keysAndValues)); + } + + @Test(expected=JSApplicationIllegalArgumentException.class) + public void testImageInvalidResizeMode() { + ReactImageManager viewManager = new ReactImageManager(); + ReactImageView view = viewManager.createViewInstance(mThemeContext); + viewManager.updateProperties(view, buildStyles("resizeMode", "pancakes")); + } + + @Test + public void testBorderColor() { + ReactImageManager viewManager = new ReactImageManager(); + ReactImageView view = viewManager.createViewInstance(mThemeContext); + viewManager.updateProperties(view, buildStyles("src", "http://mysite.com/mypic.jpg")); + + viewManager.updateProperties(view, buildStyles("borderColor", Color.argb(0, 0, 255, 255))); + int borderColor = view.getHierarchy().getRoundingParams().getBorderColor(); + assertEquals(0, Color.alpha(borderColor)); + assertEquals(0, Color.red(borderColor)); + assertEquals(255, Color.green(borderColor)); + assertEquals(255, Color.blue(borderColor)); + + viewManager.updateProperties(view, buildStyles("borderColor", Color.argb(0, 255, 50, 128))); + borderColor = view.getHierarchy().getRoundingParams().getBorderColor(); + assertEquals(0, Color.alpha(borderColor)); + assertEquals(255, Color.red(borderColor)); + assertEquals(50, Color.green(borderColor)); + assertEquals(128, Color.blue(borderColor)); + + viewManager.updateProperties(view, buildStyles("borderColor", null)); + borderColor = view.getHierarchy().getRoundingParams().getBorderColor(); + assertEquals(0, Color.alpha(borderColor)); + assertEquals(0, Color.red(borderColor)); + assertEquals(0, Color.green(borderColor)); + assertEquals(0, Color.blue(borderColor)); + } + + @Test + public void testRoundedCorners() { + ReactImageManager viewManager = new ReactImageManager(); + ReactImageView view = viewManager.createViewInstance(mThemeContext); + viewManager.updateProperties(view, buildStyles("src", "http://mysite.com/mypic.jpg")); + + // We can't easily verify if rounded corner was honored or not, this tests simply verifies + // we're not crashing.. + viewManager.updateProperties(view, buildStyles("borderRadius", (double) 10)); + viewManager.updateProperties(view, buildStyles("borderRadius", (double) 0)); + viewManager.updateProperties(view, buildStyles("borderRadius", null)); + } + + @Test + public void testTintColor() { + ReactImageManager viewManager = new ReactImageManager(); + ReactImageView view = viewManager.createViewInstance(mThemeContext); + assertNull(view.getColorFilter()); + viewManager.updateProperties(view, buildStyles("tintColor", Color.argb(50, 0, 0, 255))); + // Can't actually assert the specific color so this is the next best thing. + // Does the color filter now exist? + assertNotNull(view.getColorFilter()); + viewManager.updateProperties(view, buildStyles("tintColor", null)); + assertNull(view.getColorFilter()); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java new file mode 100644 index 000000000..890d0c0be --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/views/text/ReactTextTest.java @@ -0,0 +1,386 @@ +/** + * 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.views.text; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.annotation.TargetApi; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.AbsoluteSizeSpan; +import android.view.Choreographer; +import android.widget.TextView; + +import com.facebook.react.ReactRootView; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactTestHelper; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.SimpleArray; +import com.facebook.react.bridge.SimpleMap; +import com.facebook.react.uimanager.ReactChoreographer; +import com.facebook.react.uimanager.UIImplementation; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.uimanager.ViewProps; + +import org.junit.Before; +import org.junit.Test; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.Robolectric; +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; + +/** + * Tests for {@link UIManagerModule} specifically for React Text/RawText. + */ +@PrepareForTest({Arguments.class, ReactChoreographer.class}) +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ReactTextTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private ArrayList mPendingChoreographerCallbacks; + + @Before + public void setUp() { + PowerMockito.mockStatic(Arguments.class, ReactChoreographer.class); + + ReactChoreographer choreographerMock = mock(ReactChoreographer.class); + 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)); + } + + @Test + public void testFontSizeApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_SIZE, 21.0), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + AbsoluteSizeSpan sizeSpan = getSingleSpan( + (TextView) rootView.getChildAt(0), AbsoluteSizeSpan.class); + assertThat(sizeSpan.getSize()).isEqualTo(21); + } + + @Test + public void testBoldFontApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_WEIGHT, "bold"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView)rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getWeight() & Typeface.BOLD).isNotZero(); + assertThat(customStyleSpan.getStyle() & Typeface.ITALIC).isZero(); + } + + @Test + public void testNumericBoldFontApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_WEIGHT, "500"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getWeight() & Typeface.BOLD).isNotZero(); + assertThat(customStyleSpan.getStyle() & Typeface.ITALIC).isZero(); + } + + @Test + public void testItalicFontApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_STYLE, "italic"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getStyle() & Typeface.ITALIC).isNotZero(); + assertThat(customStyleSpan.getWeight() & Typeface.BOLD).isZero(); + } + + @Test + public void testBoldItalicFontApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_WEIGHT, "bold", ViewProps.FONT_STYLE, "italic"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getStyle() & Typeface.ITALIC).isNotZero(); + assertThat(customStyleSpan.getWeight() & Typeface.BOLD).isNotZero(); + } + + @Test + public void testNormalFontWeightApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_WEIGHT, "normal"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getWeight() & Typeface.BOLD).isZero(); + } + + @Test + public void testNumericNormalFontWeightApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_WEIGHT, "200"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getWeight() & Typeface.BOLD).isZero(); + } + + @Test + public void testNormalFontStyleApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_STYLE, "normal"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getStyle() & Typeface.ITALIC).isZero(); + } + + @Test + public void testFontFamilyStyleApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_FAMILY, "sans-serif"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getFontFamily()).isEqualTo("sans-serif"); + assertThat(customStyleSpan.getStyle() & Typeface.ITALIC).isZero(); + assertThat(customStyleSpan.getWeight() & Typeface.BOLD).isZero(); + } + + @Test + public void testFontFamilyBoldStyleApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_FAMILY, "sans-serif", ViewProps.FONT_WEIGHT, "bold"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getFontFamily()).isEqualTo("sans-serif"); + assertThat(customStyleSpan.getStyle() & Typeface.ITALIC).isZero(); + assertThat(customStyleSpan.getWeight() & Typeface.BOLD).isNotZero(); + } + + @Test + public void testFontFamilyItalicStyleApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.FONT_FAMILY, "sans-serif", ViewProps.FONT_STYLE, "italic"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getFontFamily()).isEqualTo("sans-serif"); + assertThat(customStyleSpan.getStyle() & Typeface.ITALIC).isNotZero(); + assertThat(customStyleSpan.getWeight() & Typeface.BOLD).isZero(); + } + + @Test + public void testFontFamilyBoldItalicStyleApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of( + ViewProps.FONT_FAMILY, "sans-serif", + ViewProps.FONT_WEIGHT, "500", + ViewProps.FONT_STYLE, "italic"), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + CustomStyleSpan customStyleSpan = + getSingleSpan((TextView) rootView.getChildAt(0), CustomStyleSpan.class); + assertThat(customStyleSpan.getFontFamily()).isEqualTo("sans-serif"); + assertThat(customStyleSpan.getStyle() & Typeface.ITALIC).isNotZero(); + assertThat(customStyleSpan.getWeight() & Typeface.BOLD).isNotZero(); + } + + @Test + public void testBackgroundColorStyleApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.BACKGROUND_COLOR, Color.BLUE), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + Drawable backgroundDrawable = ((TextView) rootView.getChildAt(0)).getBackground(); + assertThat(((ColorDrawable) backgroundDrawable).getColor()).isEqualTo(Color.BLUE); + } + + // JELLY_BEAN is needed for TextView#getMaxLines(), which is OK, because in the actual code we + // only use TextView#setMaxLines() which exists since API Level 1. + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + @Test + public void testMaxLinesApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = createText( + uiManager, + SimpleMap.of(ViewProps.NUMBER_OF_LINES, 2), + SimpleMap.of(ReactTextShadowNode.PROP_TEXT, "test text")); + + TextView textView = (TextView) rootView.getChildAt(0); + assertThat(textView.getText().toString()).isEqualTo("test text"); + assertThat(textView.getMaxLines()).isEqualTo(2); + assertThat(textView.getEllipsize()).isEqualTo(TextUtils.TruncateAt.END); + } + + /** + * Make sure TextView has exactly one span and that span has given type. + */ + private static TSPAN getSingleSpan(TextView textView, Class spanClass) { + Spanned text = (Spanned) textView.getText(); + TSPAN[] spans = text.getSpans(0, text.length(), spanClass); + assertThat(spans).hasSize(1); + return spans[0]; + } + + private ReactRootView createText( + UIManagerModule uiManager, + SimpleMap textProps, + SimpleMap rawTextProps) { + ReactRootView rootView = new ReactRootView(RuntimeEnvironment.application); + int rootTag = uiManager.addMeasuredRootView(rootView); + int textTag = rootTag + 1; + int rawTextTag = textTag + 1; + + uiManager.createView( + textTag, + ReactTextViewManager.REACT_CLASS, + rootTag, + textProps); + uiManager.createView( + rawTextTag, + ReactRawTextManager.REACT_CLASS, + rootTag, + rawTextProps); + + 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 void executePendingChoreographerCallbacks() { + ArrayList callbacks = + new ArrayList<>(mPendingChoreographerCallbacks); + mPendingChoreographerCallbacks.clear(); + for (Choreographer.FrameCallback frameCallback : callbacks) { + frameCallback.doFrame(0); + } + } + + public UIManagerModule getUIManagerModule() { + ReactApplicationContext reactContext = ReactTestHelper.createCatalystContextForTest(); + List viewManagers = Arrays.asList( + new ViewManager[] { + new ReactTextViewManager(), + new ReactRawTextManager(), + }); + UIManagerModule uiManagerModule = new UIManagerModule( + reactContext, + viewManagers, + new UIImplementation(reactContext, viewManagers)); + uiManagerModule.onHostResume(); + return uiManagerModule; + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java new file mode 100644 index 000000000..a42380199 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.java @@ -0,0 +1,314 @@ +/** + * 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.views.textinput; + +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.text.InputType; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.widget.EditText; + +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.ReactTestHelper; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.JSApplicationCausedNativeException; +import com.facebook.react.bridge.SimpleMap; +import com.facebook.react.uimanager.ReactStylesDiffMap; +import com.facebook.react.uimanager.DisplayMetricsHolder; +import com.facebook.react.views.text.DefaultStyleValuesUtil; +import com.facebook.react.uimanager.ThemedReactContext; + +import org.junit.Before; +import org.junit.Test; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.powermock.core.classloader.annotations.PowerMockIgnore; + +import static org.fest.assertions.api.Assertions.assertThat; + +/** + * Verify {@link EditText} view property being applied properly by {@link ReactTextInputManager} + */ +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ReactTextInputPropertyTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private ReactApplicationContext mContext; + private CatalystInstance mCatalystInstanceMock; + private ThemedReactContext mThemedContext; + private ReactTextInputManager mManager; + + @Before + public void setup() { + mContext = new ReactApplicationContext(RuntimeEnvironment.application); + mCatalystInstanceMock = ReactTestHelper.createMockCatalystInstance(); + mContext.initializeWithInstance(mCatalystInstanceMock); + mThemedContext = new ThemedReactContext(mContext, mContext); + mManager = new ReactTextInputManager(); + DisplayMetricsHolder.setDisplayMetrics(new DisplayMetrics()); + } + + public ReactStylesDiffMap buildStyles(Object... keysAndValues) { + return new ReactStylesDiffMap(SimpleMap.of(keysAndValues)); + } + + @Test + public void testAutoCorrect() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + + mManager.updateProperties(view, buildStyles()); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS).isZero(); + + mManager.updateProperties(view, buildStyles("autoCorrect", true)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT).isNotZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS).isZero(); + + mManager.updateProperties(view, buildStyles("autoCorrect", false)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS).isNotZero(); + + mManager.updateProperties(view, buildStyles("autoCorrect", null)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS).isZero(); + } + + @Test + public void testAutoCapitalize() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + + mManager.updateProperties(view, buildStyles()); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_WORDS).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS).isZero(); + + mManager.updateProperties( + view, + buildStyles("autoCapitalize", InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES).isNotZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_WORDS).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS).isZero(); + + mManager.updateProperties( + view, + buildStyles("autoCapitalize", InputType.TYPE_TEXT_FLAG_CAP_WORDS)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_WORDS).isNotZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS).isZero(); + + mManager.updateProperties( + view, + buildStyles("autoCapitalize", InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_WORDS).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS).isNotZero(); + + mManager.updateProperties( + view, + buildStyles("autoCapitalize", InputType.TYPE_CLASS_TEXT)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_WORDS).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS).isZero(); + } + + @Test + public void testPlaceholder() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + mManager.updateProperties(view, buildStyles()); + assertThat(view.getHint()).isNull(); + + mManager.updateProperties(view, buildStyles("placeholder", "sometext")); + assertThat(view.getHint()).isEqualTo("sometext"); + + mManager.updateProperties(view, buildStyles("placeholder", null)); + assertThat(view.getHint()).isNull(); + } + + @Test + public void testEditable() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + + mManager.updateProperties(view, buildStyles()); + assertThat(view.isEnabled()).isTrue(); + + mManager.updateProperties(view, buildStyles("editable", false)); + assertThat(view.isEnabled()).isFalse(); + + mManager.updateProperties(view, buildStyles("editable", null)); + assertThat(view.isEnabled()).isTrue(); + + mManager.updateProperties(view, buildStyles("editable", false)); + assertThat(view.isEnabled()).isFalse(); + + mManager.updateProperties(view, buildStyles("editable", true)); + assertThat(view.isEnabled()).isTrue(); + } + + @Test + public void testPlaceholderTextColor() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + + final ColorStateList defaultPlaceholderColorStateList = + DefaultStyleValuesUtil.getDefaultTextColorHint( + view.getContext()); + + ColorStateList colors = view.getHintTextColors(); + assertThat(colors).isEqualTo(defaultPlaceholderColorStateList); + + mManager.updateProperties(view, buildStyles("placeholderTextColor", null)); + colors = view.getHintTextColors(); + assertThat(colors).isEqualTo(defaultPlaceholderColorStateList); + + mManager.updateProperties(view, buildStyles("placeholderTextColor", Color.RED)); + colors = view.getHintTextColors(); + assertThat(colors.getDefaultColor()).isEqualTo(Color.RED); + + mManager.updateProperties(view, buildStyles("placeholderTextColor", null)); + colors = view.getHintTextColors(); + assertThat(colors).isEqualTo(defaultPlaceholderColorStateList); + } + + @Test + public void testMultiline() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + + mManager.updateProperties(view, buildStyles()); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE).isZero(); + + mManager.updateProperties(view, buildStyles("multiline", false)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE).isZero(); + + mManager.updateProperties(view, buildStyles("multiline", true)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE).isNotZero(); + + mManager.updateProperties(view, buildStyles("multiline", null)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE).isZero(); + } + + @Test + public void testNumLines() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + + mManager.updateProperties(view, buildStyles()); + assertThat(view.getMinLines()).isEqualTo(1); + + mManager.updateProperties(view, buildStyles("numberOfLines", 5)); + assertThat(view.getMinLines()).isEqualTo(5); + + mManager.updateProperties(view, buildStyles("numberOfLines", 4)); + assertThat(view.getMinLines()).isEqualTo(4); + } + + @Test + public void testKeyboardType() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + + mManager.updateProperties(view, buildStyles()); + assertThat(view.getInputType() & InputType.TYPE_CLASS_NUMBER).isZero(); + + mManager.updateProperties(view, buildStyles("keyboardType", "text")); + assertThat(view.getInputType() & InputType.TYPE_CLASS_NUMBER).isZero(); + + mManager.updateProperties(view, buildStyles("keyboardType", "numeric")); + assertThat(view.getInputType() & InputType.TYPE_CLASS_NUMBER).isNotZero(); + + mManager.updateProperties(view, buildStyles("keyboardType", "email-address")); + assertThat(view.getInputType() & InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS).isNotZero(); + + mManager.updateProperties(view, buildStyles("keyboardType", null)); + assertThat(view.getInputType() & InputType.TYPE_CLASS_NUMBER).isZero(); + } + + @Test + public void testPasswordInput() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + + mManager.updateProperties(view, buildStyles()); + assertThat(view.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD).isZero(); + + mManager.updateProperties(view, buildStyles("password", false)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD).isZero(); + + mManager.updateProperties(view, buildStyles("password", true)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD).isNotZero(); + + mManager.updateProperties(view, buildStyles("password", null)); + assertThat(view.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD).isZero(); + } + + @Test + public void testIncrementalInputTypeUpdates() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + + mManager.updateProperties(view, buildStyles()); + assertThat(view.getInputType() & InputType.TYPE_CLASS_NUMBER).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS).isZero(); + + mManager.updateProperties(view, buildStyles("multiline", true)); + assertThat(view.getInputType() & InputType.TYPE_CLASS_NUMBER).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE).isNotZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS).isZero(); + + mManager.updateProperties(view, buildStyles("autoCorrect", false)); + assertThat(view.getInputType() & InputType.TYPE_CLASS_NUMBER).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE).isNotZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS).isNotZero(); + + mManager.updateProperties(view, buildStyles("keyboardType", "NUMERIC")); + assertThat(view.getInputType() & InputType.TYPE_CLASS_NUMBER).isNotZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE).isNotZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS).isNotZero(); + + mManager.updateProperties(view, buildStyles("multiline", null)); + assertThat(view.getInputType() & InputType.TYPE_CLASS_NUMBER).isNotZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT).isZero(); + assertThat(view.getInputType() & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS).isNotZero(); + } + + @Test + public void testTextAlign() { + ReactEditText view = mManager.createViewInstance(mThemedContext); + int gravity = view.getGravity(); + assertThat(view.getGravity() & Gravity.BOTTOM).isNotEqualTo(Gravity.BOTTOM); + + mManager.updateProperties(view, buildStyles("textAlignVertical", "bottom")); + assertThat(view.getGravity() & Gravity.BOTTOM).isEqualTo(Gravity.BOTTOM); + + mManager.updateProperties( + view, + buildStyles("textAlign", "right", "textAlignVertical", "top")); + assertThat(view.getGravity() & Gravity.BOTTOM).isNotEqualTo(Gravity.BOTTOM); + assertThat(view.getGravity() & (Gravity.RIGHT | Gravity.TOP)) + .isEqualTo(Gravity.RIGHT | Gravity.TOP); + + mManager.updateProperties( + view, + buildStyles("textAlignVertical", null)); + assertThat(view.getGravity() & Gravity.RIGHT).isEqualTo(Gravity.RIGHT); + assertThat(view.getGravity() & Gravity.TOP).isNotEqualTo(Gravity.TOP); + + mManager.updateProperties(view, buildStyles("textAlign", null)); + assertThat(view.getGravity()).isEqualTo(gravity); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/textinput/TextInputTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/TextInputTest.java new file mode 100644 index 000000000..56d79f8bf --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/views/textinput/TextInputTest.java @@ -0,0 +1,191 @@ +/** + * 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.views.textinput; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.view.Choreographer; +import android.widget.EditText; + +import com.facebook.react.ReactRootView; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactTestHelper; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.SimpleArray; +import com.facebook.react.bridge.SimpleMap; +import com.facebook.react.uimanager.ReactChoreographer; +import com.facebook.react.uimanager.UIImplementation; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.uimanager.ViewProps; + +import org.junit.Before; +import org.junit.Test; +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.Robolectric; +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; + +/** + * Tests for TextInput. + */ +@PrepareForTest({Arguments.class, ReactChoreographer.class}) +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class TextInputTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + private ArrayList mPendingChoreographerCallbacks; + + @Before + public void setUp() { + PowerMockito.mockStatic(Arguments.class, ReactChoreographer.class); + + ReactChoreographer choreographerMock = mock(ReactChoreographer.class); + 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)); + } + + @Test + public void testPropsApplied() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = new ReactRootView(RuntimeEnvironment.application); + int rootTag = uiManager.addMeasuredRootView(rootView); + int textInputTag = rootTag + 1; + final String hintStr = "placeholder text"; + + uiManager.createView( + textInputTag, + ReactTextInputManager.REACT_CLASS, + rootTag, + SimpleMap.of( + ViewProps.FONT_SIZE, 13.37, ViewProps.HEIGHT, 20.0, "placeholder", hintStr)); + + uiManager.manageChildren( + rootTag, + null, + null, + SimpleArray.of(textInputTag), + SimpleArray.of(0), + null); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + EditText editText = (EditText) rootView.getChildAt(0); + assertThat(editText.getHint()).isEqualTo(hintStr); + assertThat(editText.getTextSize()).isEqualTo((float) Math.ceil(13.37)); + assertThat(editText.getHeight()).isEqualTo(20); + } + + @Test + public void testPropsUpdate() { + UIManagerModule uiManager = getUIManagerModule(); + + ReactRootView rootView = new ReactRootView(RuntimeEnvironment.application); + int rootTag = uiManager.addMeasuredRootView(rootView); + int textInputTag = rootTag + 1; + final String hintStr = "placeholder text"; + + uiManager.createView( + textInputTag, + ReactTextInputManager.REACT_CLASS, + rootTag, + SimpleMap.of( + ViewProps.FONT_SIZE, 13.37, ViewProps.HEIGHT, 20.0, "placeholder", hintStr)); + + uiManager.manageChildren( + rootTag, + null, + null, + SimpleArray.of(textInputTag), + SimpleArray.of(0), + null); + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + EditText editText = (EditText) rootView.getChildAt(0); + assertThat(editText.getHint()).isEqualTo(hintStr); + assertThat(editText.getTextSize()).isEqualTo((float) Math.ceil(13.37)); + assertThat(editText.getHeight()).isEqualTo(20); + + final String hintStr2 = "such hint"; + uiManager.updateView( + textInputTag, + ReactTextInputManager.REACT_CLASS, + SimpleMap.of( + ViewProps.FONT_SIZE, 26.74, ViewProps.HEIGHT, 40.0, "placeholder", hintStr2)); + + uiManager.onBatchComplete(); + executePendingChoreographerCallbacks(); + + EditText updatedEditText = (EditText) rootView.getChildAt(0); + assertThat(updatedEditText.getHint()).isEqualTo(hintStr2); + assertThat(updatedEditText.getTextSize()).isEqualTo((float) Math.ceil(26.74f)); + assertThat(updatedEditText.getHeight()).isEqualTo(40); + } + + private void executePendingChoreographerCallbacks() { + ArrayList callbacks = + new ArrayList<>(mPendingChoreographerCallbacks); + mPendingChoreographerCallbacks.clear(); + for (Choreographer.FrameCallback frameCallback : callbacks) { + frameCallback.doFrame(0); + } + } + + public UIManagerModule getUIManagerModule() { + ReactApplicationContext reactContext = ReactTestHelper.createCatalystContextForTest(); + List viewManagers = Arrays.asList( + new ViewManager[] { + new ReactTextInputManager(), + }); + UIManagerModule uiManagerModule = new UIManagerModule( + reactContext, + viewManagers, + new UIImplementation(reactContext, viewManagers)); + uiManagerModule.onHostResume(); + return uiManagerModule; + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.java b/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.java new file mode 100644 index 000000000..9b79b2594 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/views/view/ColorUtilTest.java @@ -0,0 +1,53 @@ +/** + * 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.views.view; + +import android.graphics.PixelFormat; + +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 static org.junit.Assert.*; + +/** + * Based on Fresco's DrawableUtilsTest (https://github.com/facebook/fresco). + */ +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class ColorUtilTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Test + public void testMultiplyColorAlpha() { + assertEquals(0x00123456, ColorUtil.multiplyColorAlpha(0xC0123456, 0)); + assertEquals(0x07123456, ColorUtil.multiplyColorAlpha(0xC0123456, 10)); + assertEquals(0x96123456, ColorUtil.multiplyColorAlpha(0xC0123456, 200)); + assertEquals(0xC0123456, ColorUtil.multiplyColorAlpha(0xC0123456, 255)); + } + + @Test + public void testGetOpacityFromColor() { + assertEquals(PixelFormat.TRANSPARENT, ColorUtil.getOpacityFromColor(0x00000000)); + assertEquals(PixelFormat.TRANSPARENT, ColorUtil.getOpacityFromColor(0x00123456)); + assertEquals(PixelFormat.TRANSPARENT, ColorUtil.getOpacityFromColor(0x00FFFFFF)); + assertEquals(PixelFormat.TRANSLUCENT, ColorUtil.getOpacityFromColor(0xC0000000)); + assertEquals(PixelFormat.TRANSLUCENT, ColorUtil.getOpacityFromColor(0xC0123456)); + assertEquals(PixelFormat.TRANSLUCENT, ColorUtil.getOpacityFromColor(0xC0FFFFFF)); + assertEquals(PixelFormat.OPAQUE, ColorUtil.getOpacityFromColor(0xFF000000)); + assertEquals(PixelFormat.OPAQUE, ColorUtil.getOpacityFromColor(0xFF123456)); + assertEquals(PixelFormat.OPAQUE, ColorUtil.getOpacityFromColor(0xFFFFFFFF)); + } +}