| // 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.common.truth.Truth.assertThat; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.anyString; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.ArgumentMatchers.same; |
| import static org.mockito.Mockito.doReturn; |
| 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.drawable.ColorDrawable; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.view.ViewGroup.LayoutParams; |
| import android.widget.FrameLayout; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import com.google.android.libraries.feed.api.host.config.DebugBehavior; |
| 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.DebugLogger.MessageType; |
| import com.google.android.libraries.feed.piet.PietStylesHelper.PietStylesHelperFactory; |
| import com.google.android.libraries.feed.piet.TemplateBinder.TemplateAdapterModel; |
| import com.google.android.libraries.feed.piet.host.ActionHandler; |
| import com.google.android.libraries.feed.piet.host.ActionHandler.ActionType; |
| import com.google.android.libraries.feed.piet.host.AssetProvider; |
| import com.google.android.libraries.feed.piet.host.CustomElementProvider; |
| import com.google.android.libraries.feed.piet.host.EventLogger; |
| import com.google.android.libraries.feed.piet.host.HostBindingProvider; |
| import com.google.android.libraries.feed.piet.ui.RoundedCornerMaskCache; |
| import com.google.search.now.ui.piet.ActionsProto.Action; |
| import com.google.search.now.ui.piet.ActionsProto.Actions; |
| import com.google.search.now.ui.piet.ActionsProto.VisibilityAction; |
| import com.google.search.now.ui.piet.BindingRefsProto.ActionsBindingRef; |
| import com.google.search.now.ui.piet.ElementsProto.BindingContext; |
| 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.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.GridRow; |
| import com.google.search.now.ui.piet.ElementsProto.ImageElement; |
| import com.google.search.now.ui.piet.ElementsProto.TemplateInvocation; |
| import com.google.search.now.ui.piet.ErrorsProto.ErrorCode; |
| import com.google.search.now.ui.piet.GradientsProto.Fill; |
| import com.google.search.now.ui.piet.ImagesProto.Image; |
| import com.google.search.now.ui.piet.PietAndroidSupport.ShardingControl; |
| import com.google.search.now.ui.piet.PietProto.Frame; |
| import com.google.search.now.ui.piet.PietProto.PietSharedState; |
| import com.google.search.now.ui.piet.PietProto.Stylesheet; |
| import com.google.search.now.ui.piet.PietProto.Template; |
| import com.google.search.now.ui.piet.RoundedCornersProto.RoundedCorners; |
| import com.google.search.now.ui.piet.ShadowsProto.ElevationShadow; |
| import com.google.search.now.ui.piet.ShadowsProto.Shadow; |
| 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.GravityVertical; |
| import com.google.search.now.ui.piet.StylesProto.Style; |
| import com.google.search.now.ui.piet.StylesProto.StyleIdsStack; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Captor; |
| import org.mockito.Mock; |
| import org.robolectric.Robolectric; |
| import org.robolectric.RobolectricTestRunner; |
| |
| /** Tests of the {@link FrameAdapterImpl}. */ |
| @RunWith(RobolectricTestRunner.class) |
| public class FrameAdapterImplTest { |
| private static final Content DEFAULT_CONTENT = |
| Content.newBuilder() |
| .setElement( |
| Element.newBuilder() |
| .setElementList( |
| ElementList.newBuilder() |
| .addContents( |
| Content.newBuilder().setElement(Element.getDefaultInstance())))) |
| .build(); |
| private static final int FRAME_WIDTH = 321; |
| |
| @Mock private AssetProvider assetProvider; |
| @Mock private ElementAdapterFactory adapterFactory; |
| @Mock private TemplateBinder templateBinder; |
| @Mock private FrameContext frameContext; |
| @Mock private DebugBehavior debugBehavior; |
| @Mock private DebugLogger debugLogger; |
| @Mock private StyleProvider styleProvider; |
| @Mock private ElementAdapter<? extends View, ?> elementAdapter; |
| @Mock private ElementAdapter<? extends View, ?> templateAdapter; |
| @Mock private CustomElementProvider customElementProvider; |
| @Mock private ActionHandler actionHandler; |
| @Mock private HostProviders hostProviders; |
| @Mock private EventLogger eventLogger; |
| @Mock private PietStylesHelperFactory stylesHelpers; |
| @Mock private RoundedCornerMaskCache maskCache; |
| |
| @Captor private ArgumentCaptor<LayoutParams> layoutParamsCaptor; |
| |
| private Context context; |
| private List<PietSharedState> pietSharedStates; |
| private AdapterParameters adapterParameters; |
| |
| private FrameAdapterImpl frameAdapter; |
| |
| @Before |
| public void setUp() throws Exception { |
| initMocks(this); |
| context = Robolectric.buildActivity(Activity.class).get(); |
| adapterParameters = |
| new AdapterParameters( |
| null, |
| Suppliers.of(new FrameLayout(context)), |
| hostProviders, |
| new ParameterizedTextEvaluator(new FakeClock()), |
| adapterFactory, |
| templateBinder, |
| new FakeClock(), |
| stylesHelpers, |
| maskCache, |
| false, |
| false); |
| when(elementAdapter.getView()).thenReturn(new LinearLayout(context)); |
| when(templateAdapter.getView()).thenReturn(new LinearLayout(context)); |
| doReturn(elementAdapter) |
| .when(adapterFactory) |
| .createAdapterForElement(any(Element.class), eq(frameContext)); |
| doReturn(templateAdapter) |
| .when(templateBinder) |
| .createAndBindTemplateAdapter(any(TemplateAdapterModel.class), eq(frameContext)); |
| when(elementAdapter.getHorizontalGravity(anyInt())).thenReturn(Gravity.CENTER_HORIZONTAL); |
| when(elementAdapter.getVerticalGravity(anyInt())).thenReturn(Gravity.BOTTOM); |
| when(templateAdapter.getHorizontalGravity(anyInt())).thenReturn(Gravity.CENTER_HORIZONTAL); |
| when(templateAdapter.getVerticalGravity(anyInt())).thenReturn(Gravity.BOTTOM); |
| when(hostProviders.getAssetProvider()).thenReturn(assetProvider); |
| when(assetProvider.isRtLSupplier()).thenReturn(Suppliers.of(false)); |
| when(frameContext.reportMessage(eq(MessageType.ERROR), anyString())) |
| .thenAnswer( |
| invocationOnMock -> { |
| throw new RuntimeException((String) invocationOnMock.getArguments()[1]); |
| }); |
| when(frameContext.getDebugLogger()).thenReturn(debugLogger); |
| when(frameContext.getDebugBehavior()).thenReturn(DebugBehavior.VERBOSE); |
| when(frameContext.makeStyleFor(any(StyleIdsStack.class))).thenReturn(styleProvider); |
| when(elementAdapter.getElementStyle()).thenReturn(styleProvider); |
| when(templateAdapter.getElementStyle()).thenReturn(styleProvider); |
| |
| pietSharedStates = new ArrayList<>(); |
| frameAdapter = |
| new FrameAdapterImpl( |
| context, adapterParameters, actionHandler, eventLogger, debugBehavior) { |
| @Override |
| FrameContext createFrameContext( |
| Frame frame, int frameWidthPx, List<PietSharedState> pietSharedStates, View view) { |
| return frameContext; |
| } |
| }; |
| } |
| |
| @Test |
| public void testCreate() { |
| LinearLayout linearLayout = frameAdapter.getFrameContainer(); |
| assertThat(linearLayout).isNotNull(); |
| LayoutParams layoutParams = linearLayout.getLayoutParams(); |
| assertThat(layoutParams).isNotNull(); |
| assertThat(layoutParams.width).isEqualTo(LayoutParams.MATCH_PARENT); |
| assertThat(layoutParams.height).isEqualTo(LayoutParams.WRAP_CONTENT); |
| } |
| |
| @Test |
| public void testGetBoundAdaptersForContent() { |
| Content content = DEFAULT_CONTENT; |
| |
| String templateId = "loaf"; |
| when(frameContext.getTemplate(templateId)).thenReturn(Template.getDefaultInstance()); |
| List<ElementAdapter<?, ?>> viewAdapters = |
| frameAdapter.getBoundAdaptersForContent(content, frameContext); |
| assertThat(viewAdapters).containsExactly(elementAdapter); |
| |
| content = |
| Content.newBuilder() |
| .setTemplateInvocation(TemplateInvocation.newBuilder().setTemplateId(templateId)) |
| .build(); |
| viewAdapters = frameAdapter.getBoundAdaptersForContent(content, frameContext); |
| assertThat(viewAdapters).isEmpty(); |
| |
| content = |
| Content.newBuilder() |
| .setTemplateInvocation( |
| TemplateInvocation.newBuilder() |
| .setTemplateId(templateId) |
| .addBindingContexts(BindingContext.getDefaultInstance())) |
| .build(); |
| viewAdapters = frameAdapter.getBoundAdaptersForContent(content, frameContext); |
| assertThat(viewAdapters).containsExactly(templateAdapter); |
| |
| content = |
| Content.newBuilder() |
| .setTemplateInvocation( |
| TemplateInvocation.newBuilder() |
| .setTemplateId(templateId) |
| .addBindingContexts( |
| BindingContext.newBuilder() |
| .addBindingValues(BindingValue.newBuilder().setBindingId("1"))) |
| .addBindingContexts( |
| BindingContext.newBuilder() |
| .addBindingValues(BindingValue.newBuilder().setBindingId("2"))) |
| .addBindingContexts( |
| BindingContext.newBuilder() |
| .addBindingValues(BindingValue.newBuilder().setBindingId("3")))) |
| .build(); |
| viewAdapters = frameAdapter.getBoundAdaptersForContent(content, frameContext); |
| assertThat(viewAdapters).containsExactly(templateAdapter, templateAdapter, templateAdapter); |
| verify(templateBinder) |
| .createAndBindTemplateAdapter( |
| new TemplateAdapterModel( |
| Template.getDefaultInstance(), |
| content.getTemplateInvocation().getBindingContexts(0)), |
| frameContext); |
| verify(templateBinder) |
| .createAndBindTemplateAdapter( |
| new TemplateAdapterModel( |
| Template.getDefaultInstance(), |
| content.getTemplateInvocation().getBindingContexts(1)), |
| frameContext); |
| verify(templateBinder) |
| .createAndBindTemplateAdapter( |
| new TemplateAdapterModel( |
| Template.getDefaultInstance(), |
| content.getTemplateInvocation().getBindingContexts(2)), |
| frameContext); |
| |
| verify(frameContext, never()).reportMessage(anyInt(), anyString()); |
| } |
| |
| /** This test sets up all real objects to ensure that real adapters are bound and unbound, etc. */ |
| @Test |
| public void testBindAndUnbind_respectsAdapterLifecycle() { |
| String templateId = "template"; |
| ElementList defaultList = |
| ElementList.newBuilder() |
| .addContents( |
| Content.newBuilder() |
| .setElement( |
| Element.newBuilder().setElementList(ElementList.getDefaultInstance()))) |
| .addContents( |
| Content.newBuilder() |
| .setElement(Element.newBuilder().setGridRow(GridRow.getDefaultInstance()))) |
| .build(); |
| AdapterParameters adapterParameters = |
| new AdapterParameters( |
| context, |
| Suppliers.of(new LinearLayout(context)), |
| hostProviders, |
| new FakeClock(), |
| false, |
| false); |
| PietSharedState pietSharedState = |
| PietSharedState.newBuilder() |
| .addTemplates( |
| Template.newBuilder() |
| .setTemplateId(templateId) |
| .setElement(Element.newBuilder().setElementList(defaultList))) |
| .build(); |
| pietSharedStates.add(pietSharedState); |
| MediaQueryHelper mediaQueryHelper = |
| new MediaQueryHelper( |
| FRAME_WIDTH, adapterParameters.hostProviders.getAssetProvider(), context); |
| PietStylesHelper stylesHelper = |
| adapterParameters.pietStylesHelperFactory.get(pietSharedStates, mediaQueryHelper); |
| FrameContext frameContext = |
| FrameContext.createFrameContext( |
| Frame.newBuilder().setStylesheet(Stylesheet.getDefaultInstance()).build(), |
| pietSharedStates, |
| stylesHelper, |
| debugBehavior, |
| new DebugLogger(), |
| actionHandler, |
| new HostProviders(assetProvider, customElementProvider, new HostBindingProvider()), |
| new FrameLayout(context)); |
| |
| frameAdapter = |
| new FrameAdapterImpl(context, adapterParameters, actionHandler, eventLogger, debugBehavior); |
| |
| Content content = |
| Content.newBuilder().setElement(Element.newBuilder().setElementList(defaultList)).build(); |
| |
| List<ElementAdapter<?, ?>> viewAdapters = |
| frameAdapter.getBoundAdaptersForContent(content, frameContext); |
| assertThat(viewAdapters).hasSize(1); |
| ElementAdapter<?, ?> viewAdapter = viewAdapters.get(0); |
| assertThat(viewAdapter.getModel()).isEqualTo(content.getElement().getElementList()); |
| frameAdapter.bindModel( |
| Frame.newBuilder().addContents(content).build(), FRAME_WIDTH, null, pietSharedStates); |
| assertThat(frameContext.getDebugLogger().getMessages(MessageType.ERROR)).isEmpty(); |
| frameAdapter.unbindModel(); |
| |
| content = |
| Content.newBuilder() |
| .setTemplateInvocation( |
| TemplateInvocation.newBuilder() |
| .setTemplateId(templateId) |
| .addBindingContexts(BindingContext.getDefaultInstance())) |
| .build(); |
| viewAdapters = frameAdapter.getBoundAdaptersForContent(content, frameContext); |
| assertThat(viewAdapters).hasSize(1); |
| viewAdapter = viewAdapters.get(0); |
| assertThat(viewAdapter.getModel()).isEqualTo(defaultList); |
| frameAdapter.bindModel( |
| Frame.newBuilder().addContents(content).build(), FRAME_WIDTH, null, pietSharedStates); |
| assertThat(frameContext.getDebugLogger().getMessages(MessageType.ERROR)).isEmpty(); |
| frameAdapter.unbindModel(); |
| } |
| |
| /** This test sets up all real objects to ensure that real adapters are bound and unbound, etc. */ |
| @Test |
| public void testBindAndUnbind_resetsStylesWhenWrapperViewIsAdded() { |
| AdapterParameters adapterParameters = |
| new AdapterParameters( |
| context, |
| Suppliers.of(new LinearLayout(context)), |
| hostProviders, |
| new FakeClock(), |
| false, |
| false); |
| PietSharedState pietSharedState = |
| PietSharedState.newBuilder() |
| .addStylesheets( |
| Stylesheet.newBuilder() |
| .setStylesheetId("stylesheet") |
| .addStyles( |
| Style.newBuilder() |
| .setStyleId("style1") |
| .setBackground(Fill.newBuilder().setColor(0x11111111)) |
| .setPadding(EdgeWidths.newBuilder().setStart(1).setTop(1)) |
| .setMargins(EdgeWidths.newBuilder().setStart(1).setTop(1)) |
| .setColor(0x11111111) |
| .setHeight(1) |
| .setWidth(1) |
| .setShadow( |
| Shadow.newBuilder() |
| .setElevationShadow( |
| ElevationShadow.newBuilder().setElevation(1))) |
| .setOpacity(0.5f)) |
| .addStyles( |
| Style.newBuilder() |
| .setStyleId("style2") |
| .setBackground(Fill.newBuilder().setColor(0x22222222)) |
| .setPadding(EdgeWidths.newBuilder().setEnd(2).setTop(2)) |
| .setMargins(EdgeWidths.newBuilder().setEnd(2).setTop(2)) |
| .setColor(0x222222) |
| .setHeight(2) |
| .setWidth(2) |
| .setShadow( |
| Shadow.newBuilder() |
| .setElevationShadow( |
| ElevationShadow.newBuilder().setElevation(2))) |
| .setOpacity(0.25f) |
| // Rounded corners will introduce an overlay. |
| .setRoundedCorners( |
| RoundedCorners.newBuilder().setRadius(2).setBitmask(2)))) |
| .build(); |
| pietSharedStates.add(pietSharedState); |
| |
| frameAdapter = |
| new FrameAdapterImpl(context, adapterParameters, actionHandler, eventLogger, debugBehavior); |
| |
| ImageElement defaultImage = |
| ImageElement.newBuilder().setImage(Image.getDefaultInstance()).build(); |
| Content content1 = |
| Content.newBuilder() |
| .setElement( |
| Element.newBuilder() |
| .setElementList( |
| ElementList.newBuilder() |
| .addContents( |
| Content.newBuilder() |
| .setElement( |
| Element.newBuilder() |
| .setGravityHorizontal(GravityHorizontal.GRAVITY_START) |
| .setGravityVertical(GravityVertical.GRAVITY_BOTTOM) |
| .setImageElement( |
| defaultImage |
| .toBuilder() |
| .setStyleReferences( |
| StyleIdsStack.newBuilder() |
| .addStyleIds("style1"))))))) |
| .build(); |
| Content content2 = |
| Content.newBuilder() |
| .setElement( |
| Element.newBuilder() |
| .setElementList( |
| ElementList.newBuilder() |
| .addContents( |
| Content.newBuilder() |
| .setElement( |
| Element.newBuilder() |
| .setGravityHorizontal(GravityHorizontal.GRAVITY_END) |
| .setImageElement( |
| defaultImage |
| .toBuilder() |
| .setStyleReferences( |
| StyleIdsStack.newBuilder() |
| .addStyleIds("style2"))))))) |
| .build(); |
| |
| // Bind to a Frame with no wrapper view |
| frameAdapter.bindModel( |
| Frame.newBuilder().setStylesheetId("stylesheet").addContents(content1).build(), |
| 0, |
| null, |
| pietSharedStates); |
| |
| ImageView imageView = |
| (ImageView) ((LinearLayout) frameAdapter.getView().getChildAt(0)).getChildAt(0); |
| LinearLayout.LayoutParams imageViewParams = |
| (LinearLayout.LayoutParams) imageView.getLayoutParams(); |
| assertThat(imageViewParams.leftMargin).isEqualTo(1); |
| assertThat(imageViewParams.topMargin).isEqualTo(1); |
| assertThat(imageViewParams.rightMargin).isEqualTo(0); |
| assertThat(imageViewParams.bottomMargin).isEqualTo(0); |
| assertThat(imageView.getPaddingStart()).isEqualTo(1); |
| assertThat(imageView.getPaddingTop()).isEqualTo(1); |
| assertThat(imageView.getPaddingEnd()).isEqualTo(0); |
| assertThat(imageView.getPaddingBottom()).isEqualTo(0); |
| assertThat(imageViewParams.height).isEqualTo(1); |
| assertThat(imageViewParams.width).isEqualTo(1); |
| assertThat(imageView.getElevation()).isWithin(0.1f).of(1); |
| assertThat(imageView.getAlpha()).isWithin(0.1f).of(0.5f); |
| |
| frameAdapter.unbindModel(); |
| |
| // Re-bind to a Frame with a wrapper view |
| // This will recycle the ImageView, but it will be within a wrapper view. |
| // Ensure that the properties on the ImageView get unset correctly. |
| frameAdapter.bindModel( |
| Frame.newBuilder().setStylesheetId("stylesheet").addContents(content2).build(), |
| 0, |
| null, |
| Collections.singletonList(pietSharedState)); |
| |
| FrameLayout wrapperView = |
| (FrameLayout) ((LinearLayout) frameAdapter.getView().getChildAt(0)).getChildAt(0); |
| LinearLayout.LayoutParams wrapperViewParams = |
| (LinearLayout.LayoutParams) wrapperView.getLayoutParams(); |
| |
| // Exactly one of the wrapper view and the image view have the specified params set. |
| assertThat(wrapperViewParams.leftMargin).isEqualTo(0); |
| assertThat(wrapperViewParams.topMargin).isEqualTo(2); |
| assertThat(wrapperViewParams.rightMargin).isEqualTo(2); |
| assertThat(wrapperViewParams.bottomMargin).isEqualTo(0); |
| assertThat(wrapperView.getPaddingStart()).isEqualTo(0); |
| assertThat(wrapperView.getPaddingTop()).isEqualTo(0); |
| assertThat(wrapperView.getPaddingEnd()).isEqualTo(0); |
| assertThat(wrapperView.getPaddingBottom()).isEqualTo(0); |
| assertThat(wrapperViewParams.height).isEqualTo(2); |
| assertThat(wrapperViewParams.width).isEqualTo(2); |
| assertThat(wrapperView.getElevation()).isWithin(0.1f).of(2); |
| assertThat(wrapperView.getAlpha()).isWithin(0.1f).of(1.0f); |
| |
| assertThat(wrapperView.getChildAt(0)).isSameInstanceAs(imageView); |
| FrameLayout.LayoutParams newImageViewParams = |
| (FrameLayout.LayoutParams) imageView.getLayoutParams(); |
| assertThat(newImageViewParams.leftMargin).isEqualTo(0); |
| assertThat(newImageViewParams.topMargin).isEqualTo(0); |
| assertThat(newImageViewParams.rightMargin).isEqualTo(0); |
| assertThat(newImageViewParams.bottomMargin).isEqualTo(0); |
| assertThat(imageView.getPaddingStart()).isEqualTo(0); |
| assertThat(imageView.getPaddingTop()).isEqualTo(2); |
| assertThat(imageView.getPaddingEnd()).isEqualTo(2); |
| assertThat(imageView.getPaddingBottom()).isEqualTo(0); |
| assertThat(newImageViewParams.height).isEqualTo(LayoutParams.MATCH_PARENT); |
| assertThat(newImageViewParams.width).isEqualTo(LayoutParams.MATCH_PARENT); |
| assertThat(imageView.getElevation()).isWithin(0.1f).of(0); |
| assertThat(imageView.getAlpha()).isWithin(0.1f).of(0.25f); |
| } |
| |
| @Test |
| public void testBindModel_defaultDimensions() { |
| Frame frame = Frame.newBuilder().addContents(DEFAULT_CONTENT).build(); |
| when(elementAdapter.getComputedWidthPx()).thenReturn(StyleProvider.DIMENSION_NOT_SET); |
| when(elementAdapter.getComputedHeightPx()).thenReturn(StyleProvider.DIMENSION_NOT_SET); |
| |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| |
| assertThat(frameAdapter.getView().getChildCount()).isEqualTo(1); |
| |
| verify(elementAdapter).setLayoutParams(layoutParamsCaptor.capture()); |
| assertThat(layoutParamsCaptor.getValue().width).isEqualTo(LayoutParams.MATCH_PARENT); |
| assertThat(layoutParamsCaptor.getValue().height).isEqualTo(LayoutParams.WRAP_CONTENT); |
| } |
| |
| @Test |
| public void testBindModel_explicitDimensions() { |
| Frame frame = Frame.newBuilder().addContents(DEFAULT_CONTENT).build(); |
| int width = 123; |
| int height = 456; |
| when(elementAdapter.getComputedWidthPx()).thenReturn(width); |
| when(elementAdapter.getComputedHeightPx()).thenReturn(height); |
| |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| |
| assertThat(frameAdapter.getView().getChildCount()).isEqualTo(1); |
| |
| verify(elementAdapter).setLayoutParams(layoutParamsCaptor.capture()); |
| assertThat(layoutParamsCaptor.getValue().width).isEqualTo(width); |
| assertThat(layoutParamsCaptor.getValue().height).isEqualTo(height); |
| } |
| |
| @Test |
| public void testBindModel_gravity() { |
| Frame frame = Frame.newBuilder().addContents(DEFAULT_CONTENT).build(); |
| when(elementAdapter.getHorizontalGravity(anyInt())).thenReturn(Gravity.CENTER_HORIZONTAL); |
| when(elementAdapter.getVerticalGravity(anyInt())).thenReturn(Gravity.BOTTOM); |
| when(elementAdapter.getGravity(anyInt())) |
| .thenReturn(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); |
| |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| |
| verify(elementAdapter).setLayoutParams(layoutParamsCaptor.capture()); |
| assertThat(((LinearLayout.LayoutParams) layoutParamsCaptor.getValue()).gravity) |
| .isEqualTo(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); |
| } |
| |
| @Test |
| public void testCreateFrameContext_recyclesPietStylesHelper() { |
| AdapterParameters adapterParameters = |
| new AdapterParameters( |
| context, |
| Suppliers.of(new LinearLayout(context)), |
| hostProviders, |
| new FakeClock(), |
| false, |
| false); |
| PietSharedState pietSharedState = |
| PietSharedState.newBuilder() |
| .addStylesheets( |
| Stylesheet.newBuilder().addStyles(Style.newBuilder().setStyleId("style"))) |
| .build(); |
| pietSharedStates.add(pietSharedState); |
| FrameAdapterImpl frameAdapter = |
| new FrameAdapterImpl(context, adapterParameters, actionHandler, eventLogger, debugBehavior); |
| |
| FrameContext frameContext1 = |
| frameAdapter.createFrameContext( |
| Frame.newBuilder().setTag("frame1").build(), |
| FRAME_WIDTH, |
| pietSharedStates, |
| frameAdapter.getView()); |
| FrameContext frameContext2 = |
| frameAdapter.createFrameContext( |
| Frame.newBuilder().setTag("frame2").build(), |
| FRAME_WIDTH, |
| pietSharedStates, |
| frameAdapter.getView()); |
| FrameContext frameContext3 = |
| frameAdapter.createFrameContext( |
| Frame.newBuilder().setTag("frame3").build(), |
| FRAME_WIDTH + 1, |
| pietSharedStates, |
| frameAdapter.getView()); |
| |
| // These should both pull the same object from the cache. |
| assertThat(frameContext1.stylesHelper).isSameInstanceAs(frameContext2.stylesHelper); |
| |
| // This one is different because of the different frame width. |
| assertThat(frameContext1.stylesHelper).isNotEqualTo(frameContext3.stylesHelper); |
| } |
| |
| @Test |
| public void testAdapterWithActions() { |
| String templateId = "lights-camera"; |
| String actionBindingId = "action"; |
| ActionsBindingRef actionBinding = |
| ActionsBindingRef.newBuilder().setBindingId(actionBindingId).build(); |
| Template templateWithActions = |
| Template.newBuilder() |
| .setTemplateId(templateId) |
| .setElement( |
| Element.newBuilder() |
| .setActionsBinding(actionBinding) |
| .setElementList(ElementList.getDefaultInstance())) |
| .build(); |
| BindingValue actionBindingValue = |
| BindingValue.newBuilder() |
| .setBindingId(actionBindingId) |
| .setActions(Actions.newBuilder().setOnClickAction(Action.getDefaultInstance())) |
| .build(); |
| Content content = |
| Content.newBuilder() |
| .setTemplateInvocation( |
| TemplateInvocation.newBuilder() |
| .setTemplateId(templateId) |
| .addBindingContexts( |
| BindingContext.newBuilder().addBindingValues(actionBindingValue))) |
| .build(); |
| Frame frame = Frame.newBuilder().addContents(content).build(); |
| PietSharedState pietSharedState = |
| PietSharedState.newBuilder().addTemplates(templateWithActions).build(); |
| |
| pietSharedStates.add(pietSharedState); |
| |
| AdapterParameters parameters = |
| new AdapterParameters( |
| context, Suppliers.of(null), hostProviders, new FakeClock(), false, false); |
| FrameAdapterImpl frameAdapter = |
| new FrameAdapterImpl(context, parameters, actionHandler, eventLogger, debugBehavior); |
| |
| frameAdapter.bindModel(frame, FRAME_WIDTH, null, pietSharedStates); |
| |
| assertThat(frameAdapter.getFrameContainer().getChildCount()).isEqualTo(1); |
| assertThat(frameAdapter.getFrameContainer().getChildAt(0).hasOnClickListeners()).isTrue(); |
| } |
| |
| @Test |
| public void testBackgroundColor() { |
| Frame frame = Frame.newBuilder().addContents(DEFAULT_CONTENT).build(); |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| styleProvider.createBackground(); |
| } |
| |
| @Test |
| public void testUnsetBackgroundIfNotDefined() { |
| Frame frame = Frame.newBuilder().addContents(DEFAULT_CONTENT).build(); |
| when(frameContext.getFrame()).thenReturn(frame); |
| |
| // Set background |
| when(styleProvider.createBackground()).thenReturn(new ColorDrawable(12345)); |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| assertThat(frameAdapter.getView().getBackground()).isNotNull(); |
| frameAdapter.unbindModel(); |
| |
| // Re-bind and check that background is unset |
| when(styleProvider.createBackground()).thenReturn(null); |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| assertThat(frameAdapter.getView().getBackground()).isNull(); |
| } |
| |
| @Test |
| public void testUnsetBackgroundIfCreateBackgroundFails() { |
| Frame frame = Frame.newBuilder().addContents(DEFAULT_CONTENT).build(); |
| when(frameContext.getFrame()).thenReturn(frame); |
| |
| // Set background |
| when(styleProvider.createBackground()).thenReturn(new ColorDrawable(12345)); |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| assertThat(frameAdapter.getView().getBackground()).isNotNull(); |
| frameAdapter.unbindModel(); |
| |
| // Re-bind and check that background is unset |
| when(styleProvider.createBackground()).thenReturn(null); |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| assertThat(frameAdapter.getView().getBackground()).isNull(); |
| } |
| |
| @Test |
| public void testRecycling_inlineSlice() { |
| Element element = |
| Element.newBuilder() |
| .setGravityVertical(GravityVertical.GRAVITY_MIDDLE) |
| .setElementList(ElementList.getDefaultInstance()) |
| .build(); |
| Frame inlineSliceFrame = |
| Frame.newBuilder().addContents(Content.newBuilder().setElement(element)).build(); |
| when(frameContext.getFrame()).thenReturn(inlineSliceFrame); |
| |
| frameAdapter.bindModel(inlineSliceFrame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| verify(adapterFactory).createAdapterForElement(element, frameContext); |
| verify(elementAdapter).bindModel(element, frameContext); |
| |
| frameAdapter.unbindModel(); |
| verify(adapterFactory).releaseAdapter(elementAdapter); |
| } |
| |
| @Test |
| public void testRecycling_templateSlice() { |
| String templateId = "bread"; |
| Template template = Template.newBuilder().setTemplateId(templateId).build(); |
| when(frameContext.getTemplate(templateId)).thenReturn(template); |
| BindingContext bindingContext = |
| BindingContext.newBuilder() |
| .addBindingValues(BindingValue.newBuilder().setBindingId("grain")) |
| .build(); |
| TemplateInvocation templateSlice = |
| TemplateInvocation.newBuilder() |
| .setTemplateId(templateId) |
| .addBindingContexts(bindingContext) |
| .build(); |
| Frame templateSliceFrame = |
| Frame.newBuilder() |
| .addContents(Content.newBuilder().setTemplateInvocation(templateSlice)) |
| .build(); |
| when(frameContext.getFrame()).thenReturn(templateSliceFrame); |
| frameAdapter.bindModel( |
| templateSliceFrame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| TemplateAdapterModel model = new TemplateAdapterModel(template, bindingContext); |
| verify(templateBinder).createAndBindTemplateAdapter(model, frameContext); |
| |
| frameAdapter.unbindModel(); |
| verify(adapterFactory).releaseAdapter(templateAdapter); |
| } |
| |
| @Test |
| public void testErrorViewReporting() { |
| View warningView = new View(context); |
| View errorView = new View(context); |
| when(debugLogger.getReportView(MessageType.WARNING, context)).thenReturn(warningView); |
| when(debugLogger.getReportView(MessageType.ERROR, context)).thenReturn(errorView); |
| |
| Frame frame = Frame.newBuilder().addContents(DEFAULT_CONTENT).build(); |
| when(frameContext.getFrame()).thenReturn(frame); |
| |
| // Errors silenced by debug behavior |
| when(frameContext.getDebugBehavior()).thenReturn(DebugBehavior.SILENT); |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| |
| assertThat(frameAdapter.getView().getChildCount()).isEqualTo(1); |
| verify(debugLogger, never()).getReportView(anyInt(), any()); |
| |
| frameAdapter.unbindModel(); |
| |
| // Errors displayed in extra views |
| when(frameContext.getDebugBehavior()).thenReturn(DebugBehavior.VERBOSE); |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| |
| assertThat(frameAdapter.getView().getChildCount()).isEqualTo(3); |
| assertThat(frameAdapter.getView().getChildAt(1)).isSameInstanceAs(errorView); |
| assertThat(frameAdapter.getView().getChildAt(2)).isSameInstanceAs(warningView); |
| |
| frameAdapter.unbindModel(); |
| |
| // No errors |
| when(debugLogger.getReportView(MessageType.WARNING, context)).thenReturn(null); |
| when(debugLogger.getReportView(MessageType.ERROR, context)).thenReturn(null); |
| |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| |
| assertThat(frameAdapter.getView().getChildCount()).isEqualTo(1); |
| } |
| |
| @Test |
| public void testEventLoggerReporting() { |
| List<ErrorCode> errorCodes = new ArrayList<>(); |
| errorCodes.add(ErrorCode.ERR_DUPLICATE_STYLE); |
| errorCodes.add(ErrorCode.ERR_POOR_FRAME_RATE); |
| when(debugLogger.getErrorCodes()).thenReturn(errorCodes); |
| |
| Frame frame = Frame.newBuilder().addContents(DEFAULT_CONTENT).build(); |
| |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| |
| verify(eventLogger).logEvents(errorCodes); |
| } |
| |
| @Test |
| public void testEventLoggerReporting_withFatalError() { |
| List<ErrorCode> errorCodes = new ArrayList<>(); |
| errorCodes.add(ErrorCode.ERR_DUPLICATE_STYLE); |
| errorCodes.add(ErrorCode.ERR_POOR_FRAME_RATE); |
| when(debugLogger.getErrorCodes()).thenReturn(errorCodes); |
| |
| when(frameContext.makeStyleFor(any())) |
| .thenThrow(new PietFatalException(ErrorCode.ERR_UNSPECIFIED, "test exception")); |
| |
| Frame frame = Frame.newBuilder().addContents(DEFAULT_CONTENT).build(); |
| |
| frameAdapter.bindModel(frame, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| |
| verify(frameContext).makeStyleFor(any()); // Ensure an exception was thrown |
| verify(eventLogger).logEvents(errorCodes); |
| } |
| |
| @Test |
| public void testUnbindTriggersHideActions() { |
| Action frameHideAction = Action.newBuilder().build(); |
| Action elementHideAction = Action.newBuilder().build(); |
| Frame frameWithNoActions = |
| Frame.newBuilder() |
| .addContents( |
| Content.newBuilder() |
| .setElement( |
| Element.newBuilder().setElementStack(ElementStack.getDefaultInstance()))) |
| .build(); |
| Frame frameWithActions = |
| Frame.newBuilder() |
| .setActions( |
| Actions.newBuilder() |
| .addOnHideActions(VisibilityAction.newBuilder().setAction(frameHideAction))) |
| .addContents( |
| Content.newBuilder() |
| .setElement( |
| Element.newBuilder() |
| .setActions( |
| Actions.newBuilder() |
| .addOnHideActions( |
| VisibilityAction.newBuilder().setAction(elementHideAction))) |
| .setElementStack(ElementStack.getDefaultInstance()))) |
| .build(); |
| |
| // Bind to a frame with no actions so the onHide actions don't get added to activeActions |
| when(frameContext.getFrame()).thenReturn(frameWithNoActions); |
| frameAdapter.bindModel( |
| frameWithNoActions, FRAME_WIDTH, (ShardingControl) null, pietSharedStates); |
| |
| when(frameContext.getFrame()).thenReturn(frameWithActions); |
| when(frameContext.getActionHandler()).thenReturn(actionHandler); |
| frameAdapter.unbindModel(); |
| |
| verify(actionHandler) |
| .handleAction( |
| same(frameHideAction), |
| eq(ActionType.VIEW), |
| eq(frameWithActions), |
| any(View.class), |
| eq(null)); |
| verify(elementAdapter).triggerHideActions(frameContext); |
| } |
| } |