blob: e62670c0b6c5af18ec3428fd30538b8b70e19da9 [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.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
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.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import com.google.android.libraries.feed.common.functional.Suppliers;
import com.google.android.libraries.feed.common.time.testing.FakeClock;
import com.google.android.libraries.feed.piet.ElementListAdapter.KeySupplier;
import com.google.android.libraries.feed.piet.host.ActionHandler;
import com.google.android.libraries.feed.piet.host.AssetProvider;
import com.google.android.libraries.feed.testing.shadows.ExtendedShadowLinearLayout;
import com.google.search.now.ui.piet.BindingRefsProto.ElementBindingRef;
import com.google.search.now.ui.piet.BindingRefsProto.StyleBindingRef;
import com.google.search.now.ui.piet.ElementsProto.BindingValue;
import com.google.search.now.ui.piet.ElementsProto.Content;
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.ElementList;
import com.google.search.now.ui.piet.ElementsProto.ElementStack;
import com.google.search.now.ui.piet.ElementsProto.ImageElement;
import com.google.search.now.ui.piet.ElementsProto.Visibility;
import com.google.search.now.ui.piet.PietProto.Frame;
import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners;
import com.google.search.now.ui.piet.StylesProto.EdgeWidths;
import com.google.search.now.ui.piet.StylesProto.GravityHorizontal;
import com.google.search.now.ui.piet.StylesProto.StyleIdsStack;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
/** Tests of the {@link ElementListAdapter}. */
@RunWith(RobolectricTestRunner.class)
public class ElementListAdapterTest {
private static final String LIST_STYLE_ID = "manycats";
private static final StyleIdsStack LIST_STYLES =
StyleIdsStack.newBuilder().addStyleIds(LIST_STYLE_ID).build();
private static final Element DEFAULT_ELEMENT =
Element.newBuilder().setElementStack(ElementStack.getDefaultInstance()).build();
private static final Content DEFAULT_CONTENT =
Content.newBuilder().setElement(DEFAULT_ELEMENT).build();
private static final Element IMAGE_ELEMENT =
Element.newBuilder().setImageElement(ImageElement.getDefaultInstance()).build();
private static final ElementList DEFAULT_LIST =
ElementList.newBuilder().addContents(DEFAULT_CONTENT).build();
private static final ElementBindingRef ELEMENT_BINDING_REF =
ElementBindingRef.newBuilder().setBindingId("shopping").build();
private static final Element LIST_WITH_BOUND_LIST =
asElement(
ElementList.newBuilder()
.addContents(Content.newBuilder().setBoundElement(ELEMENT_BINDING_REF))
.build());
@Mock private ActionHandler actionHandler;
@Mock private FrameContext frameContext;
@Mock private StyleProvider styleProvider;
@Mock private HostProviders hostProviders;
@Mock private AssetProvider assetProvider;
private Context context;
private AdapterParameters adapterParameters;
private ElementListAdapter adapter;
@Before
public void setUp() {
initMocks(this);
context = Robolectric.buildActivity(Activity.class).get();
when(frameContext.makeStyleFor(LIST_STYLES)).thenReturn(styleProvider);
when(styleProvider.getPadding()).thenReturn(EdgeWidths.getDefaultInstance());
when(frameContext.getActionHandler()).thenReturn(actionHandler);
when(hostProviders.getAssetProvider()).thenReturn(assetProvider);
when(assetProvider.isRtL()).thenReturn(false);
when(styleProvider.getRoundedCorners()).thenReturn(RoundedCorners.getDefaultInstance());
adapterParameters =
new AdapterParameters(
context, Suppliers.of(null), hostProviders, new FakeClock(), false, false);
when(frameContext.makeStyleFor(StyleIdsStack.getDefaultInstance()))
.thenReturn(adapterParameters.defaultStyleProvider);
adapter = new KeySupplier().getAdapter(context, adapterParameters);
}
@Test
public void testViewDoesNotClip() {
assertThat(adapter.getBaseView().getClipToPadding()).isFalse();
}
@Test
public void testOnCreateAdapter_makesList() {
ElementList listWithStyles =
ElementList.newBuilder()
.setStyleReferences(LIST_STYLES)
.addContents(DEFAULT_CONTENT)
.addContents(DEFAULT_CONTENT)
.addContents(DEFAULT_CONTENT)
.build();
adapter.createAdapter(asElement(listWithStyles), frameContext);
assertThat(adapter.getBaseView().getChildCount()).isEqualTo(3);
assertThat(adapter.getBaseView().getOrientation()).isEqualTo(LinearLayout.VERTICAL);
}
@Test
public void testOnCreateAdapter_setsStyles() {
ElementList listWithStyles =
ElementList.newBuilder()
.setStyleReferences(LIST_STYLES)
.addContents(DEFAULT_CONTENT)
.build();
adapter.createAdapter(asElement(listWithStyles), frameContext);
verify(styleProvider).applyElementStyles(adapter);
}
@Config(shadows = {ExtendedShadowLinearLayout.class})
@Test
public void testTriggerActions_triggersChildren() {
Frame frame = Frame.newBuilder().setTag("Frame").build();
when(frameContext.getFrame()).thenReturn(frame);
Element baseElement =
Element.newBuilder()
.setElementList(
ElementList.newBuilder()
.addContents(
Content.newBuilder()
.setElement(
Element.newBuilder()
.setElementList(ElementList.getDefaultInstance()))))
.build();
adapter.createAdapter(baseElement, frameContext);
adapter.bindModel(baseElement, frameContext);
// Replace the child adapter so we can verify on it
ElementAdapter<?, ?> mockChildAdapter = mock(ElementAdapter.class);
adapter.childAdapters.set(0, mockChildAdapter);
ExtendedShadowLinearLayout shadowView = Shadow.extract(adapter.getView());
adapter.getView().setVisibility(View.VISIBLE);
shadowView.setAttachedToWindow(true);
View viewport = new View(context);
adapter.triggerViewActions(viewport, frameContext);
verify(mockChildAdapter).triggerViewActions(viewport, frameContext);
}
@Test
public void testOnBindModel_setsStylesOnlyIfBindingIsDefined() {
ElementList listWithStyles =
ElementList.newBuilder()
.setStyleReferences(LIST_STYLES)
.addContents(DEFAULT_CONTENT)
.build();
adapter.createAdapter(asElement(listWithStyles), frameContext);
verify(frameContext).makeStyleFor(LIST_STYLES);
// Binding an element with a different style will not apply the new style
StyleIdsStack otherStyles = StyleIdsStack.newBuilder().addStyleIds("bobcat").build();
ElementList otherListWithStyles =
ElementList.newBuilder()
.setStyleReferences(otherStyles)
.addContents(DEFAULT_CONTENT)
.build();
adapter.bindModel(asElement(otherListWithStyles), frameContext);
verify(frameContext, never()).makeStyleFor(otherStyles);
// But binding an element with a style binding will re-apply the style
StyleIdsStack otherStylesWithBinding =
StyleIdsStack.newBuilder()
.addStyleIds("bobcat")
.setStyleBinding(StyleBindingRef.newBuilder().setBindingId("lynx"))
.build();
ElementList otherListWithStyleBindings =
ElementList.newBuilder()
.setStyleReferences(otherStylesWithBinding)
.addContents(DEFAULT_CONTENT)
.build();
when(frameContext.makeStyleFor(otherStylesWithBinding)).thenReturn(styleProvider);
adapter.bindModel(asElement(otherListWithStyleBindings), frameContext);
verify(frameContext).makeStyleFor(otherStylesWithBinding);
}
@Test
public void testOnBindModel_failsWithIncompatibleModel() {
ElementList listWithThreeElements =
ElementList.newBuilder()
.setStyleReferences(LIST_STYLES)
.addContents(DEFAULT_CONTENT)
.addContents(DEFAULT_CONTENT)
.addContents(DEFAULT_CONTENT)
.build();
adapter.createAdapter(asElement(listWithThreeElements), frameContext);
adapter.bindModel(asElement(listWithThreeElements), frameContext);
adapter.unbindModel();
ElementList listWithTwoElements =
ElementList.newBuilder()
.setStyleReferences(LIST_STYLES)
.addContents(DEFAULT_CONTENT)
.addContents(DEFAULT_CONTENT)
.build();
assertThatRunnable(() -> adapter.bindModel(asElement(listWithTwoElements), frameContext))
.throwsAnExceptionOfType(IllegalStateException.class)
.that()
.hasMessageThat()
.contains("Internal error in adapters per content");
}
@Test
public void testOnBindModel_elementListBindingRecreatesAdapter() {
Element listWithOneItem =
Element.newBuilder()
.setElementList(ElementList.newBuilder().addContents(DEFAULT_CONTENT))
.build();
Element listWithTwoItems =
Element.newBuilder()
.setElementList(
ElementList.newBuilder().addContents(DEFAULT_CONTENT).addContents(DEFAULT_CONTENT))
.build();
when(frameContext.getElementBindingValue(ELEMENT_BINDING_REF))
.thenReturn(BindingValue.newBuilder().setElement(listWithOneItem).build());
adapter.createAdapter(LIST_WITH_BOUND_LIST, frameContext);
// No child adapters have been created yet.
assertThat(adapter.getBaseView().getChildCount()).isEqualTo(0);
assertThat(adapter.childAdapters).isEmpty();
when(frameContext.getElementBindingValue(ELEMENT_BINDING_REF))
.thenReturn(BindingValue.newBuilder().setElement(listWithTwoItems).build());
adapter.bindModel(LIST_WITH_BOUND_LIST, frameContext);
// The list adapter creates its one view on bind.
assertThat(((LinearLayout) adapter.getBaseView().getChildAt(0)).getChildCount()).isEqualTo(2);
adapter.unbindModel();
// The list adapter has been released.
assertThat(adapter.getBaseView().getChildCount()).isEqualTo(0);
assertThat(adapter.childAdapters).isEmpty();
when(frameContext.getElementBindingValue(ELEMENT_BINDING_REF))
.thenReturn(BindingValue.newBuilder().setElement(listWithOneItem).build());
adapter.bindModel(LIST_WITH_BOUND_LIST, frameContext);
// The list adapter can bind to a different model.
assertThat(((LinearLayout) adapter.getBaseView().getChildAt(0)).getChildCount()).isEqualTo(1);
}
@Test
public void testOnBindModel_bindingWithVisibilityGone() {
when(frameContext.getElementBindingValue(ELEMENT_BINDING_REF))
.thenReturn(
BindingValue.newBuilder()
.setBindingId(ELEMENT_BINDING_REF.getBindingId())
.setElement(Element.newBuilder().setElementList(DEFAULT_LIST))
.build());
adapter.createAdapter(LIST_WITH_BOUND_LIST, frameContext);
when(frameContext.getElementBindingValue(ELEMENT_BINDING_REF))
.thenReturn(
BindingValue.newBuilder()
.setBindingId(ELEMENT_BINDING_REF.getBindingId())
.setVisibility(Visibility.GONE)
.build());
adapter.bindModel(LIST_WITH_BOUND_LIST, frameContext);
assertThat(adapter.getBaseView().getChildCount()).isEqualTo(0);
assertThat(adapter.childAdapters).isEmpty();
}
@Test
public void testOnBindModel_bindingWithNoContent() {
when(frameContext.getElementBindingValue(ELEMENT_BINDING_REF))
.thenReturn(
BindingValue.newBuilder()
.setBindingId(ELEMENT_BINDING_REF.getBindingId())
.setElement(DEFAULT_ELEMENT)
.build());
adapter.createAdapter(LIST_WITH_BOUND_LIST, frameContext);
when(frameContext.getElementBindingValue(ELEMENT_BINDING_REF))
.thenReturn(
BindingValue.newBuilder().setBindingId(ELEMENT_BINDING_REF.getBindingId()).build());
assertThat(adapter.getBaseView().getChildCount()).isEqualTo(0);
assertThat(adapter.childAdapters).isEmpty();
}
@Test
public void testOnBindModel_bindingWithOptionalAbsent() {
when(frameContext.getElementBindingValue(ELEMENT_BINDING_REF))
.thenReturn(
BindingValue.newBuilder()
.setBindingId(ELEMENT_BINDING_REF.getBindingId())
.setElement(DEFAULT_ELEMENT)
.build());
adapter.createAdapter(LIST_WITH_BOUND_LIST, frameContext);
ElementBindingRef optionalBinding = ELEMENT_BINDING_REF.toBuilder().setIsOptional(true).build();
ElementList optionalBindingList =
ElementList.newBuilder()
.addContents(Content.newBuilder().setBoundElement(optionalBinding))
.build();
when(frameContext.getElementBindingValue(optionalBinding))
.thenReturn(BindingValue.newBuilder().setBindingId(optionalBinding.getBindingId()).build());
adapter.bindModel(asElement(optionalBindingList), frameContext);
assertThat(adapter.getBaseView().getChildCount()).isEqualTo(0);
assertThat(adapter.childAdapters).isEmpty();
}
@Test
public void testOnBindModel_setsLayoutParams() {
ElementList listWithStyles =
ElementList.newBuilder()
.addContents(
Content.newBuilder()
.setElement(
DEFAULT_ELEMENT
.toBuilder()
.setStyleReferences(LIST_STYLES)
.setGravityHorizontal(GravityHorizontal.GRAVITY_CENTER)))
.build();
adapter.createAdapter(asElement(listWithStyles), frameContext);
adapter.childAdapters.get(0).widthPx = 123;
adapter.childAdapters.get(0).heightPx = 456;
adapter.bindModel(asElement(listWithStyles), frameContext);
assertThat(adapter.getBaseView().getChildCount()).isEqualTo(1);
assertThat(adapter.getBaseView().getChildAt(0).getLayoutParams())
.isInstanceOf(LinearLayout.LayoutParams.class);
assertThat(((LayoutParams) adapter.getBaseView().getChildAt(0).getLayoutParams()).gravity)
.isEqualTo(Gravity.CENTER_HORIZONTAL);
assertThat(((LayoutParams) adapter.getBaseView().getChildAt(0).getLayoutParams()).width)
.isEqualTo(123);
assertThat(((LayoutParams) adapter.getBaseView().getChildAt(0).getLayoutParams()).height)
.isEqualTo(456);
}
@Test
public void testOnBindModel_setsMargins() {
String marginsStyleId = "spacecat";
StyleIdsStack marginsStyles = StyleIdsStack.newBuilder().addStyleIds(marginsStyleId).build();
Element elementWithMargins =
Element.newBuilder()
.setStyleReferences(marginsStyles)
.setElementStack(ElementStack.getDefaultInstance())
.build();
ElementList listWithStyles =
ElementList.newBuilder()
.addContents(Content.newBuilder().setElement(elementWithMargins))
.build();
when(frameContext.makeStyleFor(marginsStyles)).thenReturn(styleProvider);
adapter.createAdapter(asElement(listWithStyles), frameContext);
adapter.bindModel(asElement(listWithStyles), frameContext);
// Assert that applyMargins is called on the child's layout params
ArgumentCaptor<MarginLayoutParams> capturedLayoutParams =
ArgumentCaptor.forClass(MarginLayoutParams.class);
verify(styleProvider).applyMargins(eq(context), capturedLayoutParams.capture());
assertThat(adapter.getBaseView().getChildCount()).isEqualTo(1);
assertThat(adapter.getBaseView().getChildAt(0).getLayoutParams())
.isInstanceOf(MarginLayoutParams.class);
assertThat(adapter.getBaseView().getChildAt(0).getLayoutParams())
.isSameInstanceAs(capturedLayoutParams.getValue());
}
@Test
public void testReleaseAdapter() {
ElementList listWithStyles =
ElementList.newBuilder()
.setStyleReferences(LIST_STYLES)
.addContents(DEFAULT_CONTENT)
.addContents(DEFAULT_CONTENT)
.addContents(DEFAULT_CONTENT)
.build();
adapter.createAdapter(asElement(listWithStyles), frameContext);
adapter.bindModel(asElement(listWithStyles), frameContext);
adapter.releaseAdapter();
assertThat(adapter.getBaseView().getChildCount()).isEqualTo(0);
assertThat(adapter.childAdapters).isEmpty();
}
@Test
public void testGetVerticalGravity_noModel() {
assertThat(adapter.getVerticalGravity(Gravity.CLIP_VERTICAL)).isEqualTo(Gravity.CLIP_VERTICAL);
}
@Test
public void testGetStyleIdsStack() {
ElementList listWithStyles =
ElementList.newBuilder()
.setStyleReferences(LIST_STYLES)
.addContents(DEFAULT_CONTENT)
.build();
adapter.createAdapter(asElement(listWithStyles), frameContext);
assertThat(adapter.getElementStyleIdsStack()).isEqualTo(LIST_STYLES);
}
@Test
public void testGetModelFromElement() {
ElementList model =
ElementList.newBuilder()
.setStyleReferences(StyleIdsStack.newBuilder().addStyleIds("spacer"))
.build();
Element elementWithModel = Element.newBuilder().setElementList(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 ElementList");
Element emptyElement = Element.getDefaultInstance();
assertThatRunnable(() -> adapter.getModelFromElement(emptyElement))
.throwsAnExceptionOfType(PietFatalException.class)
.that()
.hasMessageThat()
.contains("Missing ElementList");
}
@Test
public void testSetLayoutParams() {
ElementList listWithStyles =
ElementList.newBuilder()
.addContents(
Content.newBuilder()
.setElement(
DEFAULT_ELEMENT
.toBuilder()
.setStyleReferences(LIST_STYLES)
.setGravityHorizontal(GravityHorizontal.GRAVITY_CENTER)))
.build();
when(styleProvider.getWidthSpecPx(context)).thenReturn(123);
when(styleProvider.getHeightSpecPx(context)).thenReturn(456);
adapter.createAdapter(asElement(listWithStyles), frameContext);
LayoutParams layoutParams =
new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
adapter.setLayoutParams(layoutParams);
assertThat(((LayoutParams) adapter.getBaseView().getChildAt(0).getLayoutParams()).gravity)
.isEqualTo(Gravity.CENTER_HORIZONTAL);
assertThat(((LayoutParams) adapter.getBaseView().getChildAt(0).getLayoutParams()).width)
.isEqualTo(123);
assertThat(((LayoutParams) adapter.getBaseView().getChildAt(0).getLayoutParams()).height)
.isEqualTo(456);
}
@Test
public void testSetLayoutParams_childWidthSet() {
int childWidth = 5;
ElementList listWithStyles =
ElementList.newBuilder()
.addContents(Content.newBuilder().setElement(IMAGE_ELEMENT))
.build();
adapter.createAdapter(asElement(listWithStyles), frameContext);
StyleProvider childStyleProvider = mock(StyleProvider.class);
ImageElementAdapter mockChildAdapter = mock(ImageElementAdapter.class);
when(mockChildAdapter.getComputedWidthPx()).thenReturn(childWidth);
when(mockChildAdapter.getElementStyle()).thenReturn(childStyleProvider);
when(mockChildAdapter.getHorizontalGravity(anyInt())).thenReturn(Gravity.CENTER_HORIZONTAL);
when(mockChildAdapter.getElementStyle().hasGravityHorizontal()).thenReturn(true);
adapter.childAdapters.set(0, mockChildAdapter);
LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
adapter.setLayoutParams(params);
ArgumentCaptor<LinearLayout.LayoutParams> childLayoutParamsCaptor =
ArgumentCaptor.forClass(LinearLayout.LayoutParams.class);
verify(mockChildAdapter).setLayoutParams(childLayoutParamsCaptor.capture());
LinearLayout.LayoutParams childLayoutParams = childLayoutParamsCaptor.getValue();
assertThat(childLayoutParams.width).isEqualTo(childWidth);
assertThat(childLayoutParams.gravity).isEqualTo(Gravity.CENTER_HORIZONTAL);
verify(childStyleProvider).applyMargins(context, childLayoutParams);
}
@Test
public void testSetLayoutParams_widthSetOnList() {
ElementList listWithStyles =
ElementList.newBuilder()
.addContents(Content.newBuilder().setElement(IMAGE_ELEMENT))
.build();
adapter.createAdapter(asElement(listWithStyles), frameContext);
LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT);
params.weight = 1;
adapter.setLayoutParams(params);
assertThat(adapter.getBaseView().getChildCount()).isEqualTo(1);
assertThat(adapter.getBaseView().getChildAt(0).getLayoutParams())
.isInstanceOf(LinearLayout.LayoutParams.class);
assertThat(((LayoutParams) adapter.getBaseView().getChildAt(0).getLayoutParams()).width)
.isEqualTo(ViewGroup.LayoutParams.MATCH_PARENT);
assertThat(((LayoutParams) adapter.getBaseView().getChildAt(0).getLayoutParams()).height)
.isEqualTo(ViewGroup.LayoutParams.WRAP_CONTENT);
}
private static Element asElement(ElementList elementList) {
return Element.newBuilder().setElementList(elementList).build();
}
}