blob: dc2480b5f7cfcb4616e429f5e822a85267cfe1a6 [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.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.view.Gravity;
import android.view.View;
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.testing.shadows.ExtendedShadowView;
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.PietProto.Frame;
import com.google.search.now.ui.piet.StylesProto.GravityHorizontal;
import com.google.search.now.ui.piet.StylesProto.GravityVertical;
import java.util.HashSet;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
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 ViewUtils}. */
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ExtendedShadowView.class})
public class ViewUtilsTest {
private final Context context = Robolectric.buildActivity(Activity.class).get();
private static final Frame DEFAULT_FRAME = Frame.newBuilder().setTag("Frame").build();
private static final Action DEFAULT_ACTION = Action.getDefaultInstance();
private static final Actions DEFAULT_ACTIONS =
Actions.newBuilder().setOnClickAction(DEFAULT_ACTION).build();
private static final Actions LONG_CLICK_ACTIONS =
Actions.newBuilder().setOnLongClickAction(DEFAULT_ACTION).build();
private static final Action PARTIAL_VIEW_ACTION = Action.newBuilder().build();
private static final Action FULL_VIEW_ACTION = Action.newBuilder().build();
private static final Actions VIEW_ACTIONS =
Actions.newBuilder()
.addOnViewActions(
VisibilityAction.newBuilder()
.setProportionVisible(0.01f)
.setAction(PARTIAL_VIEW_ACTION))
.addOnViewActions(
VisibilityAction.newBuilder().setProportionVisible(1.00f).setAction(FULL_VIEW_ACTION))
.build();
// Triggers when more than 1% of the view is visible
private static final VisibilityAction VIEW_ACTION =
VisibilityAction.newBuilder()
.setProportionVisible(0.01f)
.setAction(Action.newBuilder().build())
.build();
// Triggers when more than 1% of the view is hidden
private static final VisibilityAction HIDE_ACTION =
VisibilityAction.newBuilder()
.setProportionVisible(0.99f)
.setAction(Action.newBuilder().build())
.build();
private static final Actions VIEW_AND_HIDE_ACTIONS =
Actions.newBuilder().addOnViewActions(VIEW_ACTION).addOnHideActions(HIDE_ACTION).build();
@Mock private ActionHandler mockActionHandler;
@Mock private FrameContext mockFrameContext;
@Mock private View.OnClickListener mockListener;
@Mock private View.OnLongClickListener mockLongClickListener;
private final View view = new View(context);
private final View viewport = new View(context);
private final ExtendedShadowView viewShadow = Shadow.extract(view);
private final ExtendedShadowView viewportShadow = Shadow.extract(viewport);
private final Set<VisibilityAction> activeActions = new HashSet<>();
@Before
public void setUp() {
initMocks(this);
when(mockFrameContext.getFrame()).thenReturn(DEFAULT_FRAME);
when(mockFrameContext.getActionHandler()).thenReturn(mockActionHandler);
}
@Test
public void testGravity() {
assertThat(
ViewUtils.pietGravityToGravity(
GravityHorizontal.GRAVITY_CENTER, GravityVertical.GRAVITY_BOTTOM))
.isEqualTo(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
assertThat(
ViewUtils.pietGravityToGravity(
GravityHorizontal.GRAVITY_END, GravityVertical.GRAVITY_VERTICAL_UNSPECIFIED))
.isEqualTo(Gravity.END);
assertThat(
ViewUtils.pietGravityToGravity(
GravityHorizontal.GRAVITY_HORIZONTAL_UNSPECIFIED, GravityVertical.GRAVITY_MIDDLE))
.isEqualTo(Gravity.START | Gravity.CENTER_VERTICAL);
}
@Test
public void testHorizontalGravity() {
assertThat(ViewUtils.gravityHorizontalToGravity(GravityHorizontal.GRAVITY_START))
.isEqualTo(Gravity.START);
assertThat(ViewUtils.gravityHorizontalToGravity(GravityHorizontal.GRAVITY_CENTER))
.isEqualTo(Gravity.CENTER_HORIZONTAL);
assertThat(ViewUtils.gravityHorizontalToGravity(GravityHorizontal.GRAVITY_END))
.isEqualTo(Gravity.END);
assertThat(
ViewUtils.gravityHorizontalToGravity(GravityHorizontal.GRAVITY_HORIZONTAL_UNSPECIFIED))
.isEqualTo(Gravity.START);
}
@Test
public void testVerticalGravity() {
assertThat(ViewUtils.gravityVerticalToGravity(GravityVertical.GRAVITY_TOP))
.isEqualTo(Gravity.TOP);
assertThat(ViewUtils.gravityVerticalToGravity(GravityVertical.GRAVITY_MIDDLE))
.isEqualTo(Gravity.CENTER_VERTICAL);
assertThat(ViewUtils.gravityVerticalToGravity(GravityVertical.GRAVITY_BOTTOM))
.isEqualTo(Gravity.BOTTOM);
assertThat(ViewUtils.gravityVerticalToGravity(GravityVertical.GRAVITY_VERTICAL_UNSPECIFIED))
.isEqualTo(Gravity.NO_GRAVITY);
}
@Test
public void testSetOnClickActions_success() {
ViewUtils.setOnClickActions(DEFAULT_ACTIONS, view, mockFrameContext);
assertThat(view.hasOnClickListeners()).isTrue();
view.callOnClick();
verify(mockActionHandler)
.handleAction(DEFAULT_ACTION, ActionType.CLICK, DEFAULT_FRAME, view, null);
assertThat(view.getForeground()).isInstanceOf(RippleDrawable.class);
}
@Test
public void testSetOnLongClickActions_success() {
ViewUtils.setOnClickActions(LONG_CLICK_ACTIONS, view, mockFrameContext);
view.performLongClick();
verify(mockActionHandler)
.handleAction(DEFAULT_ACTION, ActionType.LONG_CLICK, DEFAULT_FRAME, view, null);
assertThat(view.getForeground()).isInstanceOf(RippleDrawable.class);
}
@Test
public void testSetOnClickActions_noOnClickActionsDefinedClearsActions() {
view.setOnClickListener(mockListener);
assertThat(view.hasOnClickListeners()).isTrue();
ViewUtils.setOnClickActions(Actions.getDefaultInstance(), view, mockFrameContext);
assertViewNotClickable();
assertThat(view.getForeground()).isNull();
}
@Test
public void testSetOnClickActions_noOnLongClickActionsDefinedClearsActions() {
view.setOnLongClickListener(mockLongClickListener);
assertThat(view.isLongClickable()).isTrue();
ViewUtils.setOnClickActions(Actions.getDefaultInstance(), view, mockFrameContext);
assertThat(view.isLongClickable()).isFalse();
assertThat(view.getForeground()).isNull();
}
@Test
public void testClearOnClickActions_success() {
view.setOnClickListener(mockListener);
assertThat(view.hasOnClickListeners()).isTrue();
ViewUtils.clearOnClickActions(view);
assertViewNotClickable();
}
@Test
public void testClearOnLongClickActions_success() {
view.setOnLongClickListener(mockLongClickListener);
assertThat(view.isLongClickable()).isTrue();
ViewUtils.clearOnLongClickActions(view);
assertThat(view.isLongClickable()).isFalse();
}
@Test
public void testSetAndClearClickActions() {
ViewUtils.setOnClickActions(
Actions.newBuilder()
.setOnClickAction(DEFAULT_ACTION)
.setOnLongClickAction(DEFAULT_ACTION)
.build(),
view,
mockFrameContext);
ViewUtils.setOnClickActions(Actions.getDefaultInstance(), view, mockFrameContext);
assertViewNotClickable();
assertThat(view.isLongClickable()).isFalse();
assertThat(view.getForeground()).isNull();
}
@Test
public void testViewActions_notVisible() {
setupFullViewScenario();
view.setVisibility(View.INVISIBLE);
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verifyZeroInteractions(mockActionHandler);
}
@Test
public void testViewActions_notAttached() {
setupFullViewScenario();
viewShadow.setAttachedToWindow(false);
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verifyZeroInteractions(mockActionHandler);
}
@Test
public void testViewActions_notIntersecting() {
setupFullViewScenario();
viewportShadow.setLocationOnScreen(0, 0);
viewportShadow.setHeight(100);
viewportShadow.setWidth(100);
viewShadow.setLocationOnScreen(1000, 1000);
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verifyZeroInteractions(mockActionHandler);
}
@Test
public void testViewActions_intersectionTriggersPartialView() {
setupPartialViewScenario();
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verify(mockActionHandler)
.handleAction(
same(PARTIAL_VIEW_ACTION),
eq(ActionType.VIEW),
same(DEFAULT_FRAME),
same(view),
eq(null));
}
@Test
public void testViewActions_fullyOverlappingTriggersFullViewAndPartialView() {
setupFullViewScenario();
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verify(mockActionHandler)
.handleAction(
same(FULL_VIEW_ACTION), eq(ActionType.VIEW), same(DEFAULT_FRAME), same(view), eq(null));
verify(mockActionHandler)
.handleAction(
same(PARTIAL_VIEW_ACTION),
eq(ActionType.VIEW),
same(DEFAULT_FRAME),
same(view),
eq(null));
}
@Test
public void testViewActions_fullOverlapTriggersActions() {
setupFullViewScenario();
viewShadow.setLocationOnScreen(0, 0);
viewShadow.setWidth(100);
viewShadow.setHeight(100);
viewportShadow.setLocationOnScreen(0, 0);
viewportShadow.setWidth(100);
viewportShadow.setHeight(100);
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verify(mockActionHandler)
.handleAction(
same(FULL_VIEW_ACTION), eq(ActionType.VIEW), same(DEFAULT_FRAME), same(view), eq(null));
verify(mockActionHandler)
.handleAction(
same(PARTIAL_VIEW_ACTION),
eq(ActionType.VIEW),
same(DEFAULT_FRAME),
same(view),
eq(null));
}
@Test
public void testViewActions_noPartialViewAction() {
setupFullViewScenario();
ViewUtils.maybeTriggerViewActions(
view,
viewport,
Actions.newBuilder()
.addOnViewActions(
VisibilityAction.newBuilder()
.setProportionVisible(1.00f)
.setAction(FULL_VIEW_ACTION))
.build(),
mockActionHandler,
DEFAULT_FRAME,
activeActions);
verify(mockActionHandler)
.handleAction(
same(FULL_VIEW_ACTION), eq(ActionType.VIEW), same(DEFAULT_FRAME), same(view), eq(null));
}
@Test
public void testViewActions_noFullViewAction() {
setupFullViewScenario();
ViewUtils.maybeTriggerViewActions(
view,
viewport,
Actions.newBuilder()
.addOnViewActions(
VisibilityAction.newBuilder()
.setProportionVisible(0.01f)
.setAction(PARTIAL_VIEW_ACTION))
.build(),
mockActionHandler,
DEFAULT_FRAME,
activeActions);
verify(mockActionHandler)
.handleAction(
same(PARTIAL_VIEW_ACTION),
eq(ActionType.VIEW),
same(DEFAULT_FRAME),
same(view),
eq(null));
}
@Test
public void testViewActions_hideActionsNotTriggered() {
setupFullViewScenario();
ViewUtils.maybeTriggerViewActions(
view,
viewport,
Actions.newBuilder()
.addOnHideActions(
VisibilityAction.newBuilder()
.setProportionVisible(0.01f)
.setAction(PARTIAL_VIEW_ACTION))
.build(),
mockActionHandler,
DEFAULT_FRAME,
activeActions);
verifyZeroInteractions(mockActionHandler);
}
@Test
public void testViewActions_hideActionsTriggered() {
setupPartialViewScenario();
ViewUtils.maybeTriggerViewActions(
view,
viewport,
Actions.newBuilder()
.addOnHideActions(
VisibilityAction.newBuilder()
.setProportionVisible(0.90f)
.setAction(PARTIAL_VIEW_ACTION))
.build(),
mockActionHandler,
DEFAULT_FRAME,
activeActions);
verify(mockActionHandler)
.handleAction(
same(PARTIAL_VIEW_ACTION),
eq(ActionType.VIEW),
same(DEFAULT_FRAME),
same(view),
eq(null));
}
@Test
public void testViewActions_activeActionsPreventsTriggering_notVisible() {
activeActions.add(VIEW_ACTION);
activeActions.add(HIDE_ACTION);
setupFullViewScenario();
viewShadow.setLocationOnScreen(1000, 1000);
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_AND_HIDE_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verifyZeroInteractions(mockActionHandler);
}
@Test
public void testViewActions_activeActionsPreventsTriggering_partiallyVisible() {
setupPartialViewScenario();
activeActions.add(VIEW_ACTION);
activeActions.add(HIDE_ACTION);
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_AND_HIDE_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verifyZeroInteractions(mockActionHandler);
}
@Test
public void testViewActions_activeActionsPreventsTriggering_fullyVisible() {
setupFullViewScenario();
activeActions.add(VIEW_ACTION);
activeActions.add(HIDE_ACTION);
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_AND_HIDE_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verifyZeroInteractions(mockActionHandler);
}
@Test
public void testViewActions_notAttachedUnsetsActiveActions() {
setupFullViewScenario();
viewShadow.setAttachedToWindow(false);
activeActions.add(VIEW_ACTION);
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_AND_HIDE_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
assertThat(activeActions).containsExactly(HIDE_ACTION);
}
@Test
public void testViewActions_notVisibleUnsetsActiveActions() {
setupFullViewScenario();
view.setVisibility(View.INVISIBLE);
activeActions.add(VIEW_ACTION);
ViewUtils.maybeTriggerViewActions(
view, viewport, VIEW_AND_HIDE_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
assertThat(activeActions).containsExactly(HIDE_ACTION);
}
@Test
public void testHideActions() {
ViewUtils.triggerHideActions(
view, VIEW_AND_HIDE_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
assertThat(activeActions).containsExactly(HIDE_ACTION);
verify(mockActionHandler)
.handleAction(HIDE_ACTION.getAction(), ActionType.VIEW, DEFAULT_FRAME, view, null);
}
@Test
public void testHideActions_deduplicates() {
ViewUtils.triggerHideActions(
view, VIEW_AND_HIDE_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verify(mockActionHandler)
.handleAction(HIDE_ACTION.getAction(), ActionType.VIEW, DEFAULT_FRAME, view, null);
assertThat(activeActions).containsExactly(HIDE_ACTION);
// Hide action is not triggered again.
ViewUtils.triggerHideActions(
view, VIEW_AND_HIDE_ACTIONS, mockActionHandler, DEFAULT_FRAME, activeActions);
verify(mockActionHandler, times(1))
.handleAction(HIDE_ACTION.getAction(), ActionType.VIEW, DEFAULT_FRAME, view, null);
}
@Test
public void testApplyOverlayColor_setsColorFilter() {
int overlayColor1 = 0xFFEEDDCC;
int overlayColor2 = 0xCCDDEEFF;
Drawable original = new BitmapDrawable(Bitmap.createBitmap(12, 34, Bitmap.Config.ARGB_8888));
Drawable result1 = ViewUtils.applyOverlayColor(original, overlayColor1);
Drawable result2 = ViewUtils.applyOverlayColor(original, overlayColor2);
assertThat(result1).isNotSameInstanceAs(original);
assertThat(result1.getColorFilter())
.isEqualTo(new PorterDuffColorFilter(overlayColor1, Mode.SRC_IN));
assertThat(result2).isNotSameInstanceAs(original);
assertThat(result2.getColorFilter())
.isEqualTo(new PorterDuffColorFilter(overlayColor2, Mode.SRC_IN));
}
@Test
public void testApplyOverlayColor_nullIsNoOp() {
Drawable original = new BitmapDrawable(Bitmap.createBitmap(12, 34, Bitmap.Config.ARGB_8888));
Drawable result1 = ViewUtils.applyOverlayColor(original, null);
assertThat(result1).isSameInstanceAs(original);
assertThat(result1.getColorFilter()).isNull();
}
/** Sets up view and viewport so that view should be fully visible. */
private void setupFullViewScenario() {
view.setVisibility(View.VISIBLE);
viewShadow.setAttachedToWindow(true);
viewShadow.setLocationOnScreen(10, 10);
viewShadow.setWidth(10);
viewShadow.setHeight(10);
viewport.setVisibility(View.VISIBLE);
viewportShadow.setAttachedToWindow(true);
viewportShadow.setLocationOnScreen(0, 0);
viewportShadow.setWidth(100);
viewportShadow.setHeight(100);
activeActions.clear();
}
private void setupPartialViewScenario() {
setupFullViewScenario();
viewShadow.setHeight(1000);
}
private void assertViewNotClickable() {
assertThat(view.hasOnClickListeners()).isFalse();
assertThat(view.isClickable()).isFalse();
}
}