blob: 9033a0fe6a0599b2f75b05363e8bc2cde5990bba [file] [log] [blame]
// Copyright 2018 The Feed Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.android.libraries.feed.piet;
import static com.google.android.libraries.feed.common.testing.RunnableSubject.assertThatRunnable;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build.VERSION_CODES;
import android.text.Layout;
import android.text.TextUtils.TruncateAt;
import android.view.Gravity;
import android.view.View;
import android.widget.TextView;
import com.google.android.libraries.feed.common.functional.Consumer;
import com.google.android.libraries.feed.common.time.testing.FakeClock;
import com.google.android.libraries.feed.common.ui.LayoutUtils;
import com.google.android.libraries.feed.piet.DebugLogger.MessageType;
import com.google.android.libraries.feed.piet.TextElementAdapter.TextElementKey;
import com.google.android.libraries.feed.piet.host.AssetProvider;
import com.google.android.libraries.feed.piet.host.TypefaceProvider;
import com.google.android.libraries.feed.piet.host.TypefaceProvider.GoogleSansTypeface;
import com.google.search.now.ui.piet.BindingRefsProto.StyleBindingRef;
import com.google.search.now.ui.piet.ElementsProto.CustomElement;
import com.google.search.now.ui.piet.ElementsProto.Element;
import com.google.search.now.ui.piet.ElementsProto.TextElement;
import com.google.search.now.ui.piet.ErrorsProto.ErrorCode;
import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners;
import com.google.search.now.ui.piet.StylesProto;
import com.google.search.now.ui.piet.StylesProto.Font;
import com.google.search.now.ui.piet.StylesProto.Style;
import com.google.search.now.ui.piet.StylesProto.StyleIdsStack;
import com.google.search.now.ui.piet.StylesProto.TextAlignmentHorizontal;
import com.google.search.now.ui.piet.StylesProto.TextAlignmentVertical;
import com.google.search.now.ui.piet.StylesProto.Typeface.CommonTypeface;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Tests of the {@link TextElementAdapter}. */
@RunWith(RobolectricTestRunner.class)
public class TextElementAdapterTest {
@Mock private FrameContext frameContext;
@Mock private StyleProvider mockStyleProvider;
@Mock private HostProviders mockHostProviders;
@Mock private AssetProvider mockAssetProvider;
@Mock private TypefaceProvider mockTypefaceProvider;
private AdapterParameters adapterParameters;
private Context context;
private TextElementAdapter adapter;
private int emptyTextElementLineHeight;
@Before
public void setUp() {
initMocks(this);
context = Robolectric.buildActivity(Activity.class).get();
when(mockHostProviders.getAssetProvider()).thenReturn(mockAssetProvider);
when(mockAssetProvider.isRtL()).thenReturn(false);
when(mockStyleProvider.getRoundedCorners()).thenReturn(RoundedCorners.getDefaultInstance());
when(mockStyleProvider.getTextAlignment()).thenReturn(Gravity.START | Gravity.TOP);
adapterParameters =
new AdapterParameters(
null,
null,
mockHostProviders,
new ParameterizedTextEvaluator(new FakeClock()),
null,
null,
new FakeClock());
TextElementAdapter adapterForEmptyElement =
new TestTextElementAdapter(context, adapterParameters);
// Get emptyTextElementHeight based on a text element with no content or styles set.
Element textElement = getBaseTextElement();
adapterForEmptyElement.createAdapter(textElement, frameContext);
emptyTextElementLineHeight = adapterForEmptyElement.getBaseView().getLineHeight();
adapter = new TestTextElementAdapter(context, adapterParameters);
}
@Test
public void testHyphenationDisabled() {
assertThat(adapter.getBaseView().getBreakStrategy()).isEqualTo(Layout.BREAK_STRATEGY_SIMPLE);
}
@Test
public void testCreateAdapter_setsStyles() {
Element textElement = getBaseTextElement(mockStyleProvider);
int color = Color.RED;
int maxLines = 72;
when(mockStyleProvider.getFont()).thenReturn(Font.getDefaultInstance());
when(mockStyleProvider.getColor()).thenReturn(color);
when(mockStyleProvider.getMaxLines()).thenReturn(maxLines);
adapter.createAdapter(textElement, frameContext);
verify(mockStyleProvider).applyElementStyles(adapter);
assertThat(adapter.getBaseView().getMaxLines()).isEqualTo(maxLines);
assertThat(adapter.getBaseView().getEllipsize()).isEqualTo(TruncateAt.END);
assertThat(adapter.getBaseView().getCurrentTextColor()).isEqualTo(color);
}
@Test
public void testSetFont_usesCommonFont() {
Font font =
Font.newBuilder()
.addTypeface(
StylesProto.Typeface.newBuilder()
.setCommonTypeface(CommonTypeface.PLATFORM_DEFAULT_MEDIUM))
.addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("notused"))
.build();
TextElementKey key = new TextElementKey(font);
adapter.setValuesUsedInRecyclerKey(key, frameContext);
verify(mockAssetProvider, never())
.getTypeface(anyString(), anyBoolean(), ArgumentMatchers.<Consumer<Typeface>>any());
}
@Test
public void testSetFont_callsHost() {
Font font =
Font.newBuilder()
.addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("goodfont"))
.build();
TextElementKey key = new TextElementKey(font);
adapter.setValuesUsedInRecyclerKey(key, frameContext);
verify(mockAssetProvider, atLeastOnce())
.getTypeface(eq("goodfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
}
@Test
public void testSetFont_callsHostWithItalic() {
Font font =
Font.newBuilder()
.addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("goodfont"))
.addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("badfont"))
.setItalic(true)
.build();
TextElementKey key = new TextElementKey(font);
adapter.setValuesUsedInRecyclerKey(key, frameContext);
verify(mockAssetProvider, atLeastOnce())
.getTypeface(eq("goodfont"), eq(true), ArgumentMatchers.<Consumer<Typeface>>any());
}
@Test
public void testSetFont_callsHostWithFallback() {
Font font =
Font.newBuilder()
.addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("badfont"))
.addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("goodfont"))
.build();
TextElementKey key = new TextElementKey(font);
// Consumer accepts null for badfont
doAnswer(
answer -> {
Consumer<Typeface> typefaceConsumer = answer.getArgument(2);
typefaceConsumer.accept(null);
return null;
})
.when(mockAssetProvider)
.getTypeface(eq("badfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
// Consumer accepts hosttypeface for goodfont
Typeface hostTypeface = Typeface.create("host", Typeface.BOLD_ITALIC);
doAnswer(
answer -> {
Consumer<Typeface> typefaceConsumer = answer.getArgument(2);
typefaceConsumer.accept(hostTypeface);
return null;
})
.when(mockAssetProvider)
.getTypeface(eq("goodfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
adapter.setValuesUsedInRecyclerKey(key, frameContext);
Typeface typeface = adapter.getBaseView().getTypeface();
assertThat(typeface).isEqualTo(hostTypeface);
InOrder inOrder = inOrder(mockAssetProvider);
inOrder
.verify(mockAssetProvider, atLeastOnce())
.getTypeface(eq("badfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
inOrder
.verify(mockAssetProvider, atLeastOnce())
.getTypeface(eq("goodfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
}
// Same test as above, but on JB. Catches bugs that might only appear on older versions, like
// getting NPEs on getTypeface() (which is now fixed).
@Config(sdk = VERSION_CODES.JELLY_BEAN)
@Test
public void testSetFont_callsHostWithFallback_JB() {
Font font =
Font.newBuilder()
.addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("badfont"))
.addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("goodfont"))
.build();
TextElementKey key = new TextElementKey(font);
// Consumer accepts null for badfont
doAnswer(
answer -> {
Consumer<Typeface> typefaceConsumer = answer.getArgument(2);
typefaceConsumer.accept(null);
return null;
})
.when(mockAssetProvider)
.getTypeface(eq("badfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
// Consumer accepts hosttypeface for goodfont
Typeface hostTypeface = Typeface.create("host", Typeface.BOLD_ITALIC);
doAnswer(
answer -> {
Consumer<Typeface> typefaceConsumer = answer.getArgument(2);
typefaceConsumer.accept(hostTypeface);
return null;
})
.when(mockAssetProvider)
.getTypeface(eq("goodfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
adapter.setValuesUsedInRecyclerKey(key, frameContext);
Typeface typeface = adapter.getBaseView().getTypeface();
assertThat(typeface).isEqualTo(hostTypeface);
InOrder inOrder = inOrder(mockAssetProvider);
inOrder
.verify(mockAssetProvider, atLeastOnce())
.getTypeface(eq("badfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
inOrder
.verify(mockAssetProvider, atLeastOnce())
.getTypeface(eq("goodfont"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
}
@Test
public void testSetFont_hostReturnsNull() {
Font font =
Font.newBuilder()
.addTypeface(StylesProto.Typeface.newBuilder().setCustomTypeface("notvalid"))
.build();
doAnswer(
answer -> {
Consumer<Typeface> typefaceConsumer = answer.getArgument(2);
typefaceConsumer.accept(null);
return null;
})
.when(mockAssetProvider)
.getTypeface(eq("notvalid"), eq(false), ArgumentMatchers.<Consumer<Typeface>>any());
TextElementKey key = new TextElementKey(font);
adapter.setValuesUsedInRecyclerKey(key, frameContext);
Typeface typeface = adapter.getBaseView().getTypeface();
verify(frameContext)
.reportMessage(
MessageType.WARNING,
ErrorCode.ERR_MISSING_FONTS,
"Could not load specified typefaces.");
assertThat(typeface).isEqualTo(new TextView(context).getTypeface());
}
@Test
public void testSetFont_callsHostForGoogleSans() {
Font font =
Font.newBuilder()
.addTypeface(
StylesProto.Typeface.newBuilder()
.setCommonTypeface(CommonTypeface.GOOGLE_SANS_MEDIUM))
.build();
TextElementKey key = new TextElementKey(font);
adapter.setValuesUsedInRecyclerKey(key, frameContext);
verify(mockAssetProvider, atLeastOnce())
.getTypeface(
eq(GoogleSansTypeface.GOOGLE_SANS_MEDIUM),
eq(false),
ArgumentMatchers.<Consumer<Typeface>>any());
}
@Test
public void testSetFont_italics() {
Font font = Font.newBuilder().setItalic(true).build();
TextElementKey key = new TextElementKey(font);
adapter.setValuesUsedInRecyclerKey(key, frameContext);
Typeface typeface = adapter.getBaseView().getTypeface();
// Typeface.isBold and Typeface.isItalic don't work properly in roboelectric.
assertThat(typeface.getStyle() & Typeface.BOLD).isEqualTo(0);
assertThat(typeface.getStyle() & Typeface.ITALIC).isGreaterThan(0);
}
@Test
public void testGoogleSansEnumToStringDef() {
assertThat(TextElementAdapter.googleSansEnumToStringDef(CommonTypeface.GOOGLE_SANS_REGULAR))
.isEqualTo(GoogleSansTypeface.GOOGLE_SANS_REGULAR);
assertThat(TextElementAdapter.googleSansEnumToStringDef(CommonTypeface.GOOGLE_SANS_MEDIUM))
.isEqualTo(GoogleSansTypeface.GOOGLE_SANS_MEDIUM);
assertThat(TextElementAdapter.googleSansEnumToStringDef(CommonTypeface.PLATFORM_DEFAULT_MEDIUM))
.isEqualTo(GoogleSansTypeface.UNDEFINED);
}
// TODO Remove this test once transition to line height is complete.
@Test
public void testSetLineHeightRatio() {
float ratioToSet1 = 4.5f;
Style lineHeightStyle1 =
Style.newBuilder().setFont(Font.newBuilder().setLineHeightRatio(ratioToSet1)).build();
StyleProvider styleProvider1 = new StyleProvider(lineHeightStyle1, mockAssetProvider);
Element firstTextElement = getBaseTextElement(styleProvider1);
adapter.createAdapter(firstTextElement, frameContext);
float lineHeightRatio1 = adapter.getBaseView().getLineSpacingMultiplier();
assertThat(lineHeightRatio1).isEqualTo(ratioToSet1);
adapter.releaseAdapter();
// test that the line height ratio is updated when the adapter is created a second time
float ratioToSet2 = 2.1f;
Style lineHeightStyle2 =
Style.newBuilder().setFont(Font.newBuilder().setLineHeightRatio(ratioToSet2)).build();
StyleProvider styleProvider2 = new StyleProvider(lineHeightStyle2, mockAssetProvider);
Element secondTextElement = getBaseTextElement(styleProvider2);
adapter.createAdapter(secondTextElement, frameContext);
float lineHeightRatio2 = adapter.getBaseView().getLineSpacingMultiplier();
assertThat(lineHeightRatio2).isEqualTo(ratioToSet2);
}
@Test
public void testSetLineHeight() {
int lineHeightToSetSp = 18;
Style lineHeightStyle1 =
Style.newBuilder().setFont(Font.newBuilder().setLineHeight(lineHeightToSetSp)).build();
StyleProvider styleProvider1 = new StyleProvider(lineHeightStyle1, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider1);
adapter.createAdapter(textElement, frameContext);
TextView textView = adapter.getBaseView();
float actualLineHeightPx = textView.getLineHeight();
int actualLineHeightSp = (int) LayoutUtils.pxToSp(actualLineHeightPx, textView.getContext());
assertThat(actualLineHeightSp).isEqualTo(lineHeightToSetSp);
}
@Test
public void testGetExtraLineHeight_roundDown() {
// Extra height is 40.2px. This gets rounded down between the lines (to 40) and rounded up
// for top and bottom padding (for 21 + 20 = 41).
initializeAdapterWithExtraLineHeightPx(40.2f);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.betweenLinesExtraPx()).isEqualTo(40);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(21);
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(20);
}
@Config(sdk = VERSION_CODES.KITKAT)
@Test
public void testGetExtraLineHeight_roundDown_kitkat() {
// Extra height is 40.2px. In KitKat and lower, 40 pixels (the amount added between lines)
// will have already been added to the bottom. To get to our desired value of 21 bottom pixels,
// the actual bottom padding must be -19 (40 - 19 = 21).
initializeAdapterWithExtraLineHeightPx(40.2f);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.betweenLinesExtraPx()).isEqualTo(40);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(-19);
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(20);
}
@Test
public void testGetExtraLineHeight_noRound() {
// Extra height is 40px. 40 pixels will be added between each line, and that amount is split
// (20 and 20) to be added to the top and bottom of the text element.
initializeAdapterWithExtraLineHeightPx(40.0f);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.betweenLinesExtraPx()).isEqualTo(40);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(20);
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(20);
}
@Config(sdk = VERSION_CODES.KITKAT)
@Test
public void testGetExtraLineHeight_noRound_kitkat() {
// Extra height is 40px. In KitKat and lower, 40 pixels (the amount added between lines) will
// have already been added to the bottom. To get to our desired value of 20 bottom pixels, the
// actual bottom padding must be -20 (40 - 20 = 20).
initializeAdapterWithExtraLineHeightPx(40.0f);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.betweenLinesExtraPx()).isEqualTo(40);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(-20);
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(20);
}
@Test
public void testGetExtraLineHeight_roundUp() {
// Extra height is 40.8px. This gets rounded up between the lines (to 41) and rounded down
// for top and bottom padding (for 20 + 20 = 40).
initializeAdapterWithExtraLineHeightPx(40.8f);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.betweenLinesExtraPx()).isEqualTo(41);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(20);
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(20);
}
@Config(sdk = VERSION_CODES.KITKAT)
@Test
public void testGetExtraLineHeight_roundUp_kitkat() {
// Extra height is 40.8px. In KitKat and lower, 41 pixels (the amount added between lines)
// will have already been added to the bottom. To get to our desired value of 20 bottom pixels,
// the actual bottom padding must be -21 (41 - 21 = 20).
initializeAdapterWithExtraLineHeightPx(40.8f);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.betweenLinesExtraPx()).isEqualTo(41);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(-21);
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(20);
}
private void initializeAdapterWithExtraLineHeightPx(float lineHeightPx) {
// Line height is specified in sp, so line height px = scaledDensity x line height sp
// These tests set display density because, in order to test the rounding behavior of
// extraLineHeight, we need a lineHeight integer (in sp) that results in a decimal value in px.
int lineHeightSp = 10;
float totalLineHeightPx = emptyTextElementLineHeight + lineHeightPx;
context.getResources().getDisplayMetrics().scaledDensity = totalLineHeightPx / lineHeightSp;
Style lineHeightStyle1 =
Style.newBuilder().setFont(Font.newBuilder().setLineHeight(lineHeightSp)).build();
StyleProvider styleProvider1 = new StyleProvider(lineHeightStyle1, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider1);
adapter.createAdapter(textElement, frameContext);
}
// TODO Remove this test once transition to line height is complete.
@Test
public void testGetExtraLineHeight_lineHeightAndLineHeightRatioSet() {
// Even with line height ratio set, the code should use the line height code path, which means
// the line height should be line height (10) x scaledDensity (1.01) = 10.1.
context.getResources().getDisplayMetrics().scaledDensity = 1.01f;
Style lineHeightStyle1 =
Style.newBuilder()
.setFont(
Font.newBuilder()
.setLineHeight(10 + emptyTextElementLineHeight)
.setLineHeightRatio(2.0f))
.build();
StyleProvider styleProvider1 = new StyleProvider(lineHeightStyle1, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider1);
adapter.createAdapter(textElement, frameContext);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.betweenLinesExtraPx()).isEqualTo(10);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(6);
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(5);
}
// TODO Remove all these line height ratio tests when ratio is no longer used
@Test
public void testGetLineHeight_lineHeightRatio_currentVersion() {
// The line spacing is 150%. The extra 50% gets split between padding at the top and bottom of
// the view, so since the text size is 100, there is an extra 25 at top and bottom.
Style lineHeightStyle1 =
Style.newBuilder().setFont(Font.newBuilder().setSize(100).setLineHeightRatio(1.5f)).build();
StyleProvider styleProvider1 = new StyleProvider(lineHeightStyle1, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider1);
adapter.createAdapter(textElement, frameContext);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(25);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(25);
}
@Config(sdk = VERSION_CODES.KITKAT)
@Test
public void testGetLineHeight_lineHeightRatio_kitkat() {
// The line spacing is 150%. The extra 50% gets split between padding at the top and bottom of
// the view, so since the text size is 100, there is an extra 25 at top and bottom. In KitKat
// and lower, extra line spacing equal to that extra 50% is already added to the bottom. That
// means padding needs to be taken away from the bottom, to make bottom padding the same as top.
Style lineHeightStyle1 =
Style.newBuilder().setFont(Font.newBuilder().setSize(100).setLineHeightRatio(1.5f)).build();
StyleProvider styleProvider1 = new StyleProvider(lineHeightStyle1, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider1);
adapter.createAdapter(textElement, frameContext);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(25);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(-25);
}
@Test
public void testGetLineHeight_lineHeightRatio_lessThanOne() {
// The line spacing is 50%. The 50% deficit gets split between padding at the top and bottom of
// the view, so since the text size is 100, we take away an extra 25 at top and bottom, making
// top and bottom padding negative.
Style lineHeightStyle1 =
Style.newBuilder().setFont(Font.newBuilder().setSize(100).setLineHeightRatio(0.5f)).build();
StyleProvider styleProvider1 = new StyleProvider(lineHeightStyle1, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider1);
adapter.createAdapter(textElement, frameContext);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(-25);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(-25);
}
@Config(sdk = VERSION_CODES.KITKAT)
@Test
public void testGetLineHeight_lineHeightRatio_lessThanOne_kitkat() {
// The line spacing is 50%. The 50% deficit gets split between padding at the top and bottom of
// the view, so since the text size is 100, there is a negative 25 at top and bottom. In KitKat
// and lower, line spacing equal to that 50% deficit is already taken away from the bottom. That
// means padding needs to be added to the bottom, to make bottom padding the same as top.
Style lineHeightStyle1 =
Style.newBuilder().setFont(Font.newBuilder().setSize(100).setLineHeightRatio(0.5f)).build();
StyleProvider styleProvider1 = new StyleProvider(lineHeightStyle1, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider1);
adapter.createAdapter(textElement, frameContext);
TextElementAdapter.ExtraLineHeight extraLineHeight = adapter.getExtraLineHeight();
assertThat(extraLineHeight.topPaddingPx()).isEqualTo(-25);
assertThat(extraLineHeight.bottomPaddingPx()).isEqualTo(25);
}
@Test
public void testBind_setsTextAlignment_horizontal() {
Style style =
Style.newBuilder()
.setTextAlignmentHorizontal(TextAlignmentHorizontal.TEXT_ALIGNMENT_CENTER)
.build();
StyleProvider styleProvider = new StyleProvider(style, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider);
adapter.createAdapter(textElement, frameContext);
adapter.bindModel(textElement, frameContext);
assertThat(adapter.getBaseView().getGravity())
.isEqualTo(Gravity.CENTER_HORIZONTAL | Gravity.TOP);
}
@Test
public void testBind_setsTextAlignment_vertical() {
Style style =
Style.newBuilder()
.setTextAlignmentVertical(TextAlignmentVertical.TEXT_ALIGNMENT_BOTTOM)
.build();
StyleProvider styleProvider = new StyleProvider(style, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider);
adapter.createAdapter(textElement, frameContext);
adapter.bindModel(textElement, frameContext);
assertThat(adapter.getBaseView().getGravity()).isEqualTo(Gravity.START | Gravity.BOTTOM);
}
@Test
public void testBind_setsTextAlignment_both() {
Style style =
Style.newBuilder()
.setTextAlignmentHorizontal(TextAlignmentHorizontal.TEXT_ALIGNMENT_END)
.setTextAlignmentVertical(TextAlignmentVertical.TEXT_ALIGNMENT_MIDDLE)
.build();
StyleProvider styleProvider = new StyleProvider(style, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider);
adapter.createAdapter(textElement, frameContext);
adapter.bindModel(textElement, frameContext);
assertThat(adapter.getBaseView().getGravity()).isEqualTo(Gravity.END | Gravity.CENTER_VERTICAL);
}
@Test
public void testBind_setsTextAlignment_default() {
Style style = Style.getDefaultInstance();
StyleProvider styleProvider = new StyleProvider(style, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider);
adapter.getBaseView().setGravity(Gravity.BOTTOM | Gravity.RIGHT);
adapter.createAdapter(textElement, frameContext);
adapter.bindModel(textElement, frameContext);
assertThat(adapter.getBaseView().getGravity()).isEqualTo(Gravity.START | Gravity.TOP);
}
@Test
public void testBind_setsStylesOnlyIfBindingIsDefined() {
int maxLines = 2;
Style style = Style.newBuilder().setMaxLines(maxLines).build();
StyleProvider styleProvider = new StyleProvider(style, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider);
adapter.createAdapter(textElement, frameContext);
adapter.bindModel(textElement, frameContext);
assertThat(adapter.getBaseView().getMaxLines()).isEqualTo(maxLines);
// Styles should not change on a re-bind
adapter.unbindModel();
StyleIdsStack otherStyle = StyleIdsStack.newBuilder().addStyleIds("ignored").build();
textElement =
getBaseTextElement()
.toBuilder()
.setTextElement(TextElement.newBuilder().setStyleReferences(otherStyle))
.build();
adapter.bindModel(textElement, frameContext);
assertThat(adapter.getBaseView().getMaxLines()).isEqualTo(maxLines);
verify(frameContext, never()).makeStyleFor(otherStyle);
// Styles only change if new model has style bindings
adapter.unbindModel();
StyleIdsStack otherStyleWithBinding =
StyleIdsStack.newBuilder()
.setStyleBinding(StyleBindingRef.newBuilder().setBindingId("prionailurus"))
.build();
textElement =
getBaseTextElement()
.toBuilder()
.setTextElement(TextElement.newBuilder().setStyleReferences(otherStyleWithBinding))
.build();
adapter.bindModel(textElement, frameContext);
verify(frameContext).makeStyleFor(otherStyleWithBinding);
}
@Test
public void bindWithUpdatedDensity_shouldUpdateLineHeight() {
final int lineHeightInTextElement = 50;
context.getResources().getDisplayMetrics().scaledDensity = 1;
Style style =
Style.newBuilder()
.setFont(Font.newBuilder().setLineHeight(lineHeightInTextElement))
.build();
StyleProvider styleProvider = new StyleProvider(style, mockAssetProvider);
Element textElement = getBaseTextElement(styleProvider);
adapter.createAdapter(textElement, frameContext);
adapter.bindModel(textElement, frameContext);
assertThat(adapter.getBaseView().getLineHeight()).isEqualTo(lineHeightInTextElement);
adapter.unbindModel();
// Change line height by changing the scale density
context.getResources().getDisplayMetrics().scaledDensity = 2;
adapter.bindModel(textElement, frameContext);
// getLineHeight() still returns pixels. The number of pixels should have been updated to
// reflect the new density.
assertThat(adapter.getBaseView().getLineHeight()).isEqualTo(lineHeightInTextElement * 2);
adapter.unbindModel();
// Make sure the line height is updated again when the scale density is changed back.
context.getResources().getDisplayMetrics().scaledDensity = 1;
adapter.bindModel(textElement, frameContext);
assertThat(adapter.getBaseView().getLineHeight()).isEqualTo(lineHeightInTextElement);
}
@Test
public void testUnbind() {
Element textElement = getBaseTextElement(null);
adapter.createAdapter(textElement, frameContext);
adapter.bindModel(textElement, frameContext);
TextView adapterView = adapter.getBaseView();
adapterView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
adapterView.setText("OLD TEXT");
adapter.unbindModel();
assertThat(adapter.getBaseView()).isSameInstanceAs(adapterView);
assertThat(adapterView.getTextAlignment()).isEqualTo(View.TEXT_ALIGNMENT_GRAVITY);
assertThat(adapterView.getText().toString()).isEmpty();
}
@Test
public void testGetStyles() {
StyleIdsStack elementStyles = StyleIdsStack.newBuilder().addStyleIds("hair").build();
when(mockStyleProvider.getFont()).thenReturn(Font.getDefaultInstance());
Element textElement =
getBaseTextElement(mockStyleProvider)
.toBuilder()
.setTextElement(TextElement.newBuilder().setStyleReferences(elementStyles))
.build();
adapter.createAdapter(textElement, frameContext);
assertThat(adapter.getElementStyleIdsStack()).isSameInstanceAs(elementStyles);
}
@Test
public void testGetModelFromElement() {
TextElement model =
TextElement.newBuilder()
.setStyleReferences(StyleIdsStack.newBuilder().addStyleIds("spacer"))
.build();
Element elementWithModel = Element.newBuilder().setTextElement(model).build();
assertThat(adapter.getModelFromElement(elementWithModel)).isSameInstanceAs(model);
Element elementWithWrongModel =
Element.newBuilder().setCustomElement(CustomElement.getDefaultInstance()).build();
assertThatRunnable(() -> adapter.getModelFromElement(elementWithWrongModel))
.throwsAnExceptionOfType(PietFatalException.class)
.that()
.hasMessageThat()
.contains("Missing TextElement");
Element emptyElement = Element.getDefaultInstance();
assertThatRunnable(() -> adapter.getModelFromElement(emptyElement))
.throwsAnExceptionOfType(PietFatalException.class)
.that()
.hasMessageThat()
.contains("Missing TextElement");
}
private Element getBaseTextElement() {
return getBaseTextElement(null);
}
private Element getBaseTextElement(/*@Nullable*/ StyleProvider styleProvider) {
StyleProvider sp =
styleProvider != null ? styleProvider : adapterParameters.defaultStyleProvider;
when(frameContext.makeStyleFor(any(StyleIdsStack.class))).thenReturn(sp);
return Element.newBuilder().setTextElement(TextElement.getDefaultInstance()).build();
}
private static class TestTextElementAdapter extends TextElementAdapter {
TestTextElementAdapter(Context context, AdapterParameters parameters) {
super(context, parameters);
}
@Override
void setTextOnView(FrameContext frameContext, TextElement textElement) {}
@Override
TextElementKey createKey(Font font) {
return new TextElementKey(font);
}
}
}