/**
* 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.Drawable;
import android.os.Build;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.UnderlineSpan;
import android.view.Choreographer;
import android.widget.TextView;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.JavaOnlyArray;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactTestHelper;
import com.facebook.react.uimanager.ReactChoreographer;
import com.facebook.react.uimanager.UIImplementationProvider;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.views.view.ReactViewBackgroundDrawable;
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.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.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<Choreographer.FrameCallback> mPendingChoreographerCallbacks;
@Before
public void setUp() {
PowerMockito.mockStatic(Arguments.class, ReactChoreographer.class);
ReactChoreographer choreographerMock = mock(ReactChoreographer.class);
PowerMockito.when(Arguments.createMap()).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return new JavaOnlyMap();
}
});
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,
JavaOnlyMap.of(ViewProps.FONT_SIZE, 21.0),
JavaOnlyMap.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,
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "bold"),
JavaOnlyMap.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,
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "500"),
JavaOnlyMap.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,
JavaOnlyMap.of(ViewProps.FONT_STYLE, "italic"),
JavaOnlyMap.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,
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "bold", ViewProps.FONT_STYLE, "italic"),
JavaOnlyMap.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,
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "normal"),
JavaOnlyMap.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,
JavaOnlyMap.of(ViewProps.FONT_WEIGHT, "200"),
JavaOnlyMap.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,
JavaOnlyMap.of(ViewProps.FONT_STYLE, "normal"),
JavaOnlyMap.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,
JavaOnlyMap.of(ViewProps.FONT_FAMILY, "sans-serif"),
JavaOnlyMap.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,
JavaOnlyMap.of(ViewProps.FONT_FAMILY, "sans-serif", ViewProps.FONT_WEIGHT, "bold"),
JavaOnlyMap.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,
JavaOnlyMap.of(ViewProps.FONT_FAMILY, "sans-serif", ViewProps.FONT_STYLE, "italic"),
JavaOnlyMap.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,
JavaOnlyMap.of(
ViewProps.FONT_FAMILY, "sans-serif",
ViewProps.FONT_WEIGHT, "500",
ViewProps.FONT_STYLE, "italic"),
JavaOnlyMap.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 testTextDecorationLineUnderlineApplied() {
UIManagerModule uiManager = getUIManagerModule();
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.TEXT_DECORATION_LINE, "underline"),
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
TextView textView = (TextView) rootView.getChildAt(0);
Spanned text = (Spanned) textView.getText();
UnderlineSpan underlineSpan = getSingleSpan(textView, UnderlineSpan.class);
StrikethroughSpan[] strikeThroughSpans =
text.getSpans(0, text.length(), StrikethroughSpan.class);
assertThat(underlineSpan instanceof UnderlineSpan).isTrue();
assertThat(strikeThroughSpans).hasSize(0);
}
@Test
public void testTextDecorationLineLineThroughApplied() {
UIManagerModule uiManager = getUIManagerModule();
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.TEXT_DECORATION_LINE, "line-through"),
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
TextView textView = (TextView) rootView.getChildAt(0);
Spanned text = (Spanned) textView.getText();
UnderlineSpan[] underlineSpans =
text.getSpans(0, text.length(), UnderlineSpan.class);
StrikethroughSpan strikeThroughSpan =
getSingleSpan(textView, StrikethroughSpan.class);
assertThat(underlineSpans).hasSize(0);
assertThat(strikeThroughSpan instanceof StrikethroughSpan).isTrue();
}
@Test
public void testTextDecorationLineUnderlineLineThroughApplied() {
UIManagerModule uiManager = getUIManagerModule();
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.TEXT_DECORATION_LINE, "underline line-through"),
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
UnderlineSpan underlineSpan =
getSingleSpan((TextView) rootView.getChildAt(0), UnderlineSpan.class);
StrikethroughSpan strikeThroughSpan =
getSingleSpan((TextView) rootView.getChildAt(0), StrikethroughSpan.class);
assertThat(underlineSpan instanceof UnderlineSpan).isTrue();
assertThat(strikeThroughSpan instanceof StrikethroughSpan).isTrue();
}
@Test
public void testBackgroundColorStyleApplied() {
UIManagerModule uiManager = getUIManagerModule();
ReactRootView rootView = createText(
uiManager,
JavaOnlyMap.of(ViewProps.BACKGROUND_COLOR, Color.BLUE),
JavaOnlyMap.of(ReactTextShadowNode.PROP_TEXT, "test text"));
Drawable backgroundDrawable = ((TextView) rootView.getChildAt(0)).getBackground();
assertThat(((ReactViewBackgroundDrawable) 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,
JavaOnlyMap.of(ViewProps.NUMBER_OF_LINES, 2),
JavaOnlyMap.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> TSPAN getSingleSpan(TextView textView, Class<TSPAN> 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,
JavaOnlyMap textProps,
JavaOnlyMap 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,
JavaOnlyArray.of(rawTextTag),
JavaOnlyArray.of(0),
null);
uiManager.manageChildren(
rootTag,
null,
null,
JavaOnlyArray.of(textTag),
JavaOnlyArray.of(0),
null);
uiManager.onBatchComplete();
executePendingChoreographerCallbacks();
return rootView;
}
private void executePendingChoreographerCallbacks() {
ArrayList<Choreographer.FrameCallback> callbacks =
new ArrayList<>(mPendingChoreographerCallbacks);
mPendingChoreographerCallbacks.clear();
for (Choreographer.FrameCallback frameCallback : callbacks) {
frameCallback.doFrame(0);
}
}
public UIManagerModule getUIManagerModule() {
ReactApplicationContext reactContext = ReactTestHelper.createCatalystContextForTest();
List<ViewManager> viewManagers = Arrays.asList(
new ViewManager[] {
new ReactTextViewManager(),
new ReactRawTextManager(),
});
UIManagerModule uiManagerModule = new UIManagerModule(
reactContext,
viewManagers,
new UIImplementationProvider());
uiManagerModule.onHostResume();
return uiManagerModule;
}
}