package com.mopub.nativeads;
import android.app.Activity;
import android.graphics.Rect;
import android.os.Handler;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.Window;
import com.mopub.nativeads.VisibilityTracker.TrackingInfo;
import com.mopub.common.test.support.SdkTestRunner;
import org.fest.util.Lists;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.shadows.ShadowSystemClock;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import static android.view.ViewTreeObserver.OnPreDrawListener;
import static com.mopub.nativeads.VisibilityTracker.VisibilityChecker;
import static com.mopub.nativeads.VisibilityTracker.VisibilityTrackerListener;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(SdkTestRunner.class)
public class VisibilityTrackerTest {
private static final int MIN_PERCENTAGE_VIEWED = 50;
private Activity activity;
private VisibilityTracker subject;
private Map<View, TrackingInfo> trackedViews;
private VisibilityChecker visibilityChecker;
@Mock private VisibilityTrackerListener visibilityTrackerListener;
@Mock private View view;
@Mock private View view2;
@Mock private Handler visibilityHandler;
@Before
public void setUp() throws Exception {
trackedViews = new WeakHashMap<View, TrackingInfo>();
visibilityChecker = new VisibilityChecker();
activity = new Activity();
view = createViewMock(View.VISIBLE, 100, 100, 100, 100, true, true);
view2 = createViewMock(View.VISIBLE, 100, 100, 100, 100, true, true);
// Add a proxy listener to that makes a safe copy of the listener args.
VisibilityTrackerListener proxyListener = new VisibilityTrackerListener() {
@Override
public void onVisibilityChanged(List<View> visibleViews, List<View> invisibleViews) {
ArrayList<View> safeVisibleViews = new ArrayList<View>(visibleViews);
ArrayList<View> safeInVisibleViews = new ArrayList<View>(invisibleViews);
visibilityTrackerListener.onVisibilityChanged(safeVisibleViews, safeInVisibleViews);
}
};
subject = new VisibilityTracker(activity, trackedViews, visibilityChecker, visibilityHandler);
subject.setVisibilityTrackerListener(proxyListener);
// XXX We need this to ensure that our SystemClock starts
ShadowSystemClock.uptimeMillis();
}
@Test
public void constructor_shouldSetOnPreDrawListenerForDecorView() throws Exception {
Activity activity1 = mock(Activity.class);
Window window = mock(Window.class);
View decorView = mock(View.class);
ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(activity1.getWindow()).thenReturn(window);
when(window.getDecorView()).thenReturn(decorView);
when(decorView.getViewTreeObserver()).thenReturn(viewTreeObserver);
when(viewTreeObserver.isAlive()).thenReturn(true);
subject = new VisibilityTracker(activity1, trackedViews, visibilityChecker, visibilityHandler);
assertThat(subject.mRootView.get()).isEqualTo(decorView);
assertThat(subject.mOnPreDrawListener).isNotNull();
verify(viewTreeObserver).addOnPreDrawListener(subject.mOnPreDrawListener);
}
@Test
public void constructor_withNonAliveViewTreeObserver_shouldNotSetOnPreDrawListenerForDecorView() throws Exception {
Activity activity1 = mock(Activity.class);
Window window = mock(Window.class);
View decorView = mock(View.class);
ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(activity1.getWindow()).thenReturn(window);
when(window.getDecorView()).thenReturn(decorView);
when(decorView.getViewTreeObserver()).thenReturn(viewTreeObserver);
when(viewTreeObserver.isAlive()).thenReturn(false);
subject = new VisibilityTracker(activity1, trackedViews, visibilityChecker, visibilityHandler);
assertThat(subject.mRootView.get()).isEqualTo(decorView);
assertThat(subject.mOnPreDrawListener).isNull();
verify(viewTreeObserver, never()).addOnPreDrawListener(subject.mOnPreDrawListener);
}
@Test
public void addView_withVisibleView_shouldAddVisibleViewToTrackedViews() throws Exception {
subject.addView(view, MIN_PERCENTAGE_VIEWED);
assertThat(trackedViews).hasSize(1);
}
@Test(expected = AssertionError.class)
public void addView_whenViewIsNull_shouldThrowNPE() throws Exception {
subject.addView(null, MIN_PERCENTAGE_VIEWED);
assertThat(trackedViews).isEmpty();
}
@Test
public void removeView_shouldRemoveFromTrackedViews() throws Exception {
subject.addView(view, MIN_PERCENTAGE_VIEWED);
assertThat(trackedViews).hasSize(1);
assertThat(trackedViews).containsKey(view);
subject.removeView(view);
assertThat(trackedViews).isEmpty();
}
@Test
public void clear_shouldRemoveAllViewsFromTrackedViews_shouldRemoveMessagesFromVisibilityHandler_shouldResetIsVisibilityScheduled() throws Exception {
subject.addView(view, MIN_PERCENTAGE_VIEWED);
subject.addView(view2, MIN_PERCENTAGE_VIEWED);
assertThat(trackedViews).hasSize(2);
subject.clear();
assertThat(trackedViews).isEmpty();
verify(visibilityHandler).removeMessages(0);
}
@Test
public void destroy_shouldCallClear_shouldRemoveListenerFromDecorView() throws Exception {
Activity activity1 = mock(Activity.class);
Window window = mock(Window.class);
View decorView = mock(View.class);
ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
when(activity1.getWindow()).thenReturn(window);
when(window.getDecorView()).thenReturn(decorView);
when(decorView.getViewTreeObserver()).thenReturn(viewTreeObserver);
when(viewTreeObserver.isAlive()).thenReturn(true);
subject = new VisibilityTracker(activity1, trackedViews, visibilityChecker, visibilityHandler);
subject.addView(view, MIN_PERCENTAGE_VIEWED);
subject.addView(view2, MIN_PERCENTAGE_VIEWED);
assertThat(trackedViews).hasSize(2);
subject.destroy();
assertThat(trackedViews).isEmpty();
verify(visibilityHandler).removeMessages(0);
verify(viewTreeObserver).removeOnPreDrawListener(any(OnPreDrawListener.class));
assertThat(subject.mOnPreDrawListener).isNull();
}
@Test
public void visibilityRunnable_run_withVisibleView_shouldCallOnVisibleCallback() throws Exception {
subject.addView(view, MIN_PERCENTAGE_VIEWED);
subject.new VisibilityRunnable().run();
verify(visibilityTrackerListener).onVisibilityChanged(
Lists.newArrayList(view), Lists.<View>newArrayList());
}
@Test
public void visibilityRunnable_run_withNonVisibleView_shouldCallOnNonVisibleCallback() throws Exception {
when(view.getVisibility()).thenReturn(View.INVISIBLE);
subject.addView(view, MIN_PERCENTAGE_VIEWED);
subject.new VisibilityRunnable().run();
ArgumentCaptor<List> visibleCaptor = ArgumentCaptor.forClass(List.class);
ArgumentCaptor<List> invisibleCaptor = ArgumentCaptor.forClass(List.class);
verify(visibilityTrackerListener).onVisibilityChanged(visibleCaptor.capture(),
invisibleCaptor.capture());
assertThat(visibleCaptor.getValue().size()).isEqualTo(0);
assertThat(invisibleCaptor.getValue().size()).isEqualTo(1);
}
// VisibilityChecker tests
@Test
public void hasRequiredTimeElapsed_withElapsedTimeGreaterThanMinTimeViewed_shouldReturnTrue() throws Exception {
assertThat(visibilityChecker.hasRequiredTimeElapsed(SystemClock.uptimeMillis() - 501, 500)).isTrue();
}
@Test
public void hasRequiredTimeElapsed_withElapsedTimeLessThanMinTimeViewed_shouldReturnFalse() throws Exception {
assertThat(visibilityChecker.hasRequiredTimeElapsed(SystemClock.uptimeMillis() - 499, 500)).isFalse();
}
@Test
public void isMostlyVisible_whenParentIsNull_shouldReturnFalse() throws Exception {
view = createViewMock(View.VISIBLE, 100, 100, 100, 100, false, true);
assertThat(visibilityChecker.isVisible(view, MIN_PERCENTAGE_VIEWED)).isFalse();
}
@Test
public void isMostlyVisible_whenViewIsOffScreen_shouldReturnFalse() throws Exception {
view = createViewMock(View.VISIBLE, 100, 100, 100, 100, true, false);
assertThat(visibilityChecker.isVisible(view, MIN_PERCENTAGE_VIEWED)).isFalse();
}
@Test
public void isMostlyVisible_whenViewIsEntirelyOnScreen_shouldReturnTrue() throws Exception {
view = createViewMock(View.VISIBLE, 100, 100, 100, 100, true, true);
assertThat(visibilityChecker.isVisible(view, MIN_PERCENTAGE_VIEWED)).isTrue();
}
@Test
public void isMostlyVisible_whenViewIs50PercentVisible_shouldReturnTrue() throws Exception {
view = createViewMock(View.VISIBLE, 50, 100, 100, 100, true, true);
assertThat(visibilityChecker.isVisible(view, MIN_PERCENTAGE_VIEWED)).isTrue();
}
@Test
public void isMostlyVisible_whenViewIs49PercentVisible_shouldReturnFalse() throws Exception {
view = createViewMock(View.VISIBLE, 49, 100, 100, 100, true, true);
assertThat(visibilityChecker.isVisible(view, MIN_PERCENTAGE_VIEWED)).isFalse();
}
@Test
public void isMostlyVisible_whenVisibleAreaIsZero_shouldReturnFalse() throws Exception {
view = createViewMock(View.VISIBLE, 0, 0, 100, 100, true, true);
assertThat(visibilityChecker.isVisible(view, MIN_PERCENTAGE_VIEWED)).isFalse();
}
@Test
public void isMostlyVisible_whenViewIsInvisibleOrGone_shouldReturnFalse() throws Exception {
View view = createViewMock(View.INVISIBLE, 100, 100, 100, 100, true, true);
assertThat(visibilityChecker.isVisible(view, MIN_PERCENTAGE_VIEWED)).isFalse();
reset(view);
view = createViewMock(View.GONE, 100, 100, 100, 100, true, true);
assertThat(visibilityChecker.isVisible(view, MIN_PERCENTAGE_VIEWED)).isFalse();
}
@Test
public void isMostlyVisible_whenViewHasZeroWidthAndHeight_shouldReturnFalse() throws Exception {
view = createViewMock(View.VISIBLE, 100, 100, 0, 0, true, true);
assertThat(visibilityChecker.isVisible(view, MIN_PERCENTAGE_VIEWED)).isFalse();
}
@Test
public void isMostlyVisible_whenViewIsNull_shouldReturnFalse() throws Exception {
assertThat(visibilityChecker.isVisible(null, MIN_PERCENTAGE_VIEWED)).isFalse();
}
@Test
public void addView_shouldClearViewAfterNumAccesses() {
// Access 1 time
subject.addView(view, MIN_PERCENTAGE_VIEWED);
assertThat(trackedViews).hasSize(1);
// Access 2-49 times
for (int i = 0; i < VisibilityTracker.NUM_ACCESSES_BEFORE_TRIMMING - 2; ++i) {
subject.addView(view2, MIN_PERCENTAGE_VIEWED);
}
assertThat(trackedViews).hasSize(2);
// 50th time
subject.addView(view2, MIN_PERCENTAGE_VIEWED);
assertThat(trackedViews).hasSize(2);
// 51-99
for (int i = 0; i < VisibilityTracker.NUM_ACCESSES_BEFORE_TRIMMING - 1; ++i) {
subject.addView(view2, MIN_PERCENTAGE_VIEWED);
}
assertThat(trackedViews).hasSize(2);
// 100
subject.addView(view2, MIN_PERCENTAGE_VIEWED);
assertThat(trackedViews).hasSize(1);
}
static View createViewMock(final int visibility,
final int visibleWidth,
final int visibleHeight,
final int viewWidth,
final int viewHeight,
final boolean isParentSet,
final boolean isOnScreen) {
View view = mock(View.class);
when(view.getContext()).thenReturn(new Activity());
when(view.getVisibility()).thenReturn(visibility);
when(view.getGlobalVisibleRect(any(Rect.class)))
.thenAnswer(new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable {
Object[] args = invocationOnMock.getArguments();
Rect rect = (Rect) args[0];
rect.set(0, 0, visibleWidth, visibleHeight);
return isOnScreen;
}
});
when(view.getWidth()).thenReturn(viewWidth);
when(view.getHeight()).thenReturn(viewHeight);
if (isParentSet) {
when(view.getParent()).thenReturn(mock(ViewParent.class));
}
when(view.getViewTreeObserver()).thenCallRealMethod();
return view;
}
}