/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.litho;
import java.util.ArrayList;
import java.util.List;
import android.graphics.Rect;
import android.support.v4.util.LongSparseArray;
import com.facebook.litho.testing.testrunner.ComponentsTestRunner;
import com.facebook.litho.testing.TestLayoutComponent;
import com.facebook.litho.testing.TestViewComponent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.reflect.Whitebox;
import org.robolectric.RuntimeEnvironment;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(ComponentsTestRunner.class)
public class MountStateVisibilityEventsTest {
private static final int VISIBLE = 1;
private static final int FOCUSED = 2;
private static final int FULL_IMPRESSION = 3;
private static final int INVISIBLE = 4;
private static final int UNFOCUSED = 5;
private static final int LEFT = 0;
private static final int RIGHT = 10;
private static final int VIEWPORT_HEIGHT = 5;
private static final int VIEWPORT_WIDTH = 10;
private long mLastVisibilityOutputId = 0;
private ComponentContext mContext;
private MountState mMountState;
@Before
public void setup() {
mContext = new ComponentContext(RuntimeEnvironment.application);
ComponentTree mockComponentTree = mock(ComponentTree.class);
doReturn(mContext).when(mockComponentTree).getContext();
ComponentHost mockParent = mock(ComponentHost.class);
doReturn(VIEWPORT_WIDTH).when(mockParent).getWidth();
doReturn(VIEWPORT_HEIGHT).when(mockParent).getHeight();
LithoView attachedView = spy(new LithoView(mContext));
Whitebox.setInternalState(attachedView, "mComponentTree", mockComponentTree);
doReturn(mockParent).when(attachedView).getParent();
mMountState = new MountState(attachedView);
Whitebox.setInternalState(mMountState, "mIsDirty", false);
mLastVisibilityOutputId = 0;
}
@Test
public void testVisibleEvent() {
ComponentLifecycle mockLifecycle = createLifecycleMock();
Component<?> content = TestViewComponent.create(mContext).build();
Whitebox.setInternalState(content, "mLifecycle", mockLifecycle);
final EventHandler visibleHandler = createEventHandler(content, VISIBLE);
final List<VisibilityOutput> visibilityOutputs = new ArrayList<>();
visibilityOutputs.add(createVisibilityOutput(
content,
new Rect(LEFT, 5, RIGHT, 10),
visibleHandler,
null,
null,
null,
null));
final LayoutState layoutState = new LayoutState();
Whitebox.setInternalState(layoutState, "mVisibilityOutputs", visibilityOutputs);
mMountState.mount(layoutState, new Rect(LEFT, 0, RIGHT, 5));
checkNoVisibilityEventsDispatched(mockLifecycle);
assertEquals(0, getVisibilityItemMapSize());
mMountState.mount(layoutState, new Rect(LEFT, 5, RIGHT, 10));
verify(mockLifecycle, times(1)).dispatchOnEvent(
eq(visibleHandler),
isA(VisibleEvent.class));
assertEquals(1, getVisibilityItemMapSize());
}
@Test
public void testFocusedOccupiesHalfViewport() {
ComponentLifecycle mockLifecycle = createLifecycleMock();
Component<?> content = TestViewComponent.create(mContext).build();
Whitebox.setInternalState(content, "mLifecycle", mockLifecycle);
final EventHandler focusedHandler = createEventHandler(content, FOCUSED);
final List<VisibilityOutput> visibilityOutputs = new ArrayList<>();
visibilityOutputs.add(createVisibilityOutput(
content,
new Rect(LEFT, 5, RIGHT, 10),
null,
focusedHandler,
null,
null,
null));
final LayoutState layoutState = new LayoutState();
Whitebox.setInternalState(layoutState, "mVisibilityOutputs", visibilityOutputs);
mMountState.mount(layoutState, new Rect(LEFT, 1, RIGHT, 6));
checkNoVisibilityEventsDispatched(mockLifecycle);
mMountState.mount(layoutState, new Rect(LEFT, 3, RIGHT, 8));
verify(mockLifecycle, times(1)).dispatchOnEvent(
eq(focusedHandler),
isA(FocusedVisibleEvent.class));
}
@Test
public void testFocusedSmallerThanHalfViewport() {
ComponentLifecycle mockLifecycle = createLifecycleMock();
Component<?> content = TestViewComponent.create(mContext).build();
Whitebox.setInternalState(content, "mLifecycle", mockLifecycle);
final EventHandler focusedHandler = createEventHandler(content, FOCUSED);
final List<VisibilityOutput> visibilityOutputs = new ArrayList<>();
visibilityOutputs.add(createVisibilityOutput(
content,
new Rect(LEFT, 5, RIGHT, 8),
null,
focusedHandler,
null,
null,
null));
final LayoutState layoutState = new LayoutState();
Whitebox.setInternalState(layoutState, "mVisibilityOutputs", visibilityOutputs);
mMountState.mount(layoutState, new Rect(LEFT, 1, RIGHT, 6));
checkNoVisibilityEventsDispatched(mockLifecycle);
mMountState.mount(layoutState, new Rect(LEFT, 2, RIGHT, 7));
checkNoVisibilityEventsDispatched(mockLifecycle);
mMountState.mount(layoutState, new Rect(LEFT, 3, RIGHT, 8));
verify(mockLifecycle, times(1)).dispatchOnEvent(
eq(focusedHandler),
isA(FocusedVisibleEvent.class));
}
@Test
public void testMultipleFocusAndUnfocusEvents() {
ComponentLifecycle mockLifecycle = createLifecycleMock();
Component<?> content = TestViewComponent.create(mContext).build();
Whitebox.setInternalState(content, "mLifecycle", mockLifecycle);
final EventHandler focusedHandler = createEventHandler(content, FOCUSED);
final EventHandler unfocusedHandler = createEventHandler(content, UNFOCUSED);
final List<VisibilityOutput> visibilityOutputs = new ArrayList<>();
visibilityOutputs.add(createVisibilityOutput(
content,
new Rect(LEFT, 5, RIGHT, 12),
null,
focusedHandler,
unfocusedHandler,
null,
null));
final LayoutState layoutState = new LayoutState();
Whitebox.setInternalState(layoutState, "mVisibilityOutputs", visibilityOutputs);
//Mount test view in the middle of the view port (focused)
mMountState.mount(layoutState, new Rect(LEFT, 6, RIGHT, 11));
verify(mockLifecycle, times(1)).dispatchOnEvent(
eq(focusedHandler),
isA(FocusedVisibleEvent.class));
//Mount test view on the edge of the viewport (not focused)
mMountState.mount(layoutState, new Rect(LEFT, 11, RIGHT, 16));
verify(mockLifecycle, times(1)).dispatchOnEvent(
eq(unfocusedHandler),
isA(UnfocusedVisibleEvent.class));
//Mount test view in the middle of the view port (focused)
mMountState.mount(layoutState, new Rect(LEFT, 4, RIGHT, 9));
verify(mockLifecycle, times(2)).dispatchOnEvent(
eq(focusedHandler),
isA(FocusedVisibleEvent.class));
//Mount test view off the edge of the view port (unfocused)
mMountState.mount(layoutState, new Rect(LEFT, 1, RIGHT, 6));
verify(mockLifecycle, times(2)).dispatchOnEvent(
eq(unfocusedHandler),
isA(UnfocusedVisibleEvent.class));
}
@Test
public void testNoUnfocusEvents() {
ComponentLifecycle mockLifecycle = createLifecycleMock();
Component<?> content = TestViewComponent.create(mContext).build();
Whitebox.setInternalState(content, "mLifecycle", mockLifecycle);
final EventHandler unfocusedHandler = createEventHandler(content, UNFOCUSED);
final List<VisibilityOutput> visibilityOutputs = new ArrayList<>();
visibilityOutputs.add(createVisibilityOutput(
content,
new Rect(LEFT, 5, RIGHT, 12),
null,
null,
unfocusedHandler,
null,
null));
final LayoutState layoutState = new LayoutState();
Whitebox.setInternalState(layoutState, "mVisibilityOutputs", visibilityOutputs);
//Mount test view in the middle of the view port (focused)
mMountState.mount(layoutState, new Rect(LEFT, 6, RIGHT, 11));
verify(mockLifecycle, times(0)).dispatchOnEvent(
eq(unfocusedHandler),
isA(UnfocusedVisibleEvent.class));
}
@Test
public void testFullImpression() {
ComponentLifecycle mockLifecycle = createLifecycleMock();
Component<?> content = TestViewComponent.create(mContext).build();
Whitebox.setInternalState(content, "mLifecycle", mockLifecycle);
final EventHandler fullImpressionHandler = createEventHandler(content, FULL_IMPRESSION);
final List<VisibilityOutput> visibilityOutputs = new ArrayList<>();
visibilityOutputs.add(createVisibilityOutput(
content,
new Rect(LEFT, 5, RIGHT, 10),
null,
null,
null,
fullImpressionHandler,
null));
final LayoutState layoutState = new LayoutState();
Whitebox.setInternalState(layoutState, "mVisibilityOutputs", visibilityOutputs);
mMountState.mount(layoutState, new Rect(LEFT, 4, RIGHT, 9));
checkNoVisibilityEventsDispatched(mockLifecycle);
mMountState.mount(layoutState, new Rect(LEFT, 9, RIGHT, 14));
verify(mockLifecycle, times(1)).dispatchOnEvent(
eq(fullImpressionHandler),
isA(FullImpressionVisibleEvent.class));
}
@Test
public void testInvisibleEvent() {
ComponentLifecycle mockLifecycle = createLifecycleMock();
Component<?> content = TestViewComponent.create(mContext).build();
Whitebox.setInternalState(content, "mLifecycle", mockLifecycle);
final EventHandler invisibleHandler = createEventHandler(content, INVISIBLE);
final List<VisibilityOutput> visibilityOutputs = new ArrayList<>();
visibilityOutputs.add(createVisibilityOutput(
content,
new Rect(LEFT, 5, RIGHT, 10),
null,
null,
null,
null,
invisibleHandler));
final LayoutState layoutState = new LayoutState();
Whitebox.setInternalState(layoutState, "mVisibilityOutputs", visibilityOutputs);
mMountState.mount(layoutState, new Rect(LEFT, 5, RIGHT, 10));
checkNoVisibilityEventsDispatched(mockLifecycle);
assertEquals(1, getVisibilityItemMapSize());
mMountState.mount(layoutState, new Rect(LEFT, 0, RIGHT, 5));
verify(mockLifecycle, times(1)).dispatchOnEvent(
eq(invisibleHandler),
isA(InvisibleEvent.class));
assertEquals(0, getVisibilityItemMapSize());
}
@Test
public void testBothVisibilityEvents() {
ComponentLifecycle mockLifecycle = createLifecycleMock();
Component<?> content = TestViewComponent.create(mContext).build();
Whitebox.setInternalState(content, "mLifecycle", mockLifecycle);
final EventHandler visibleHandler = createEventHandler(content, VISIBLE);
final EventHandler invisibleHandler = createEventHandler(content, INVISIBLE);
final List<VisibilityOutput> visibilityOutputs = new ArrayList<>();
visibilityOutputs.add(createVisibilityOutput(
content,
new Rect(LEFT, 5, RIGHT, 10),
visibleHandler,
null,
null,
null,
invisibleHandler));
final LayoutState layoutState = new LayoutState();
Whitebox.setInternalState(layoutState, "mVisibilityOutputs", visibilityOutputs);
mMountState.mount(layoutState, new Rect(LEFT, 0, RIGHT, 5));
checkNoVisibilityEventsDispatched(mockLifecycle);
assertEquals(0, getVisibilityItemMapSize());
mMountState.mount(layoutState, new Rect(LEFT, 5, RIGHT, 10));
verify(mockLifecycle, times(1)).dispatchOnEvent(
eq(visibleHandler),
isA(VisibleEvent.class));
assertEquals(1, getVisibilityItemMapSize());
mMountState.mount(layoutState, new Rect(LEFT, 10, RIGHT, 15));
verify(mockLifecycle, times(1)).dispatchOnEvent(
eq(invisibleHandler),
isA(InvisibleEvent.class));
assertEquals(0, getVisibilityItemMapSize());
}
@Test
public void testVisibilityEventsMultipleOutputs() {
ComponentLifecycle mockLifecycle1 = createLifecycleMock();
ComponentLifecycle mockLifecycle2 = createLifecycleMock();
Component<?> content1 = TestViewComponent.create(mContext).build();
Component<?> content2 = TestLayoutComponent.create(mContext).build();
Whitebox.setInternalState(content1, "mLifecycle", mockLifecycle1);
Whitebox.setInternalState(content2, "mLifecycle", mockLifecycle2);
final EventHandler visibleHandler1 = createEventHandler(content1, VISIBLE);
final EventHandler invisibleHandler1 = createEventHandler(content1, INVISIBLE);
final EventHandler visibleHandler2 = createEventHandler(content2, VISIBLE);
final List<VisibilityOutput> visibilityOutputs = new ArrayList<>();
visibilityOutputs.add(createVisibilityOutput(
content1,
new Rect(LEFT, 5, RIGHT, 10),
visibleHandler1,
null,
null,
null,
invisibleHandler1));
visibilityOutputs.add(createVisibilityOutput(
content2,
new Rect(LEFT, 10, RIGHT, 15),
visibleHandler2,
null,
null,
null,
null));
final LayoutState layoutState = new LayoutState();
Whitebox.setInternalState(layoutState, "mVisibilityOutputs", visibilityOutputs);
mMountState.mount(layoutState, new Rect(LEFT, 5, RIGHT, 10));
verify(mockLifecycle1, times(1)).dispatchOnEvent(
eq(visibleHandler1),
isA(VisibleEvent.class));
checkNoVisibilityEventsDispatched(mockLifecycle2);
assertEquals(1, getVisibilityItemMapSize());
mMountState.mount(layoutState, new Rect(LEFT, 10, RIGHT, 15));
verify(mockLifecycle1, times(1)).dispatchOnEvent(
eq(invisibleHandler1),
isA(InvisibleEvent.class));
verify(mockLifecycle2, times(1)).dispatchOnEvent(
eq(visibleHandler2),
isA(VisibleEvent.class));
assertEquals(1, getVisibilityItemMapSize());
}
@Test
public void testVisibilityEventsMultipleVisible() {
ComponentLifecycle mockLifecycle1 = createLifecycleMock();
ComponentLifecycle mockLifecycle2 = createLifecycleMock();
Component<?> content1 = TestViewComponent.create(mContext).build();
Component<?> content2 = TestLayoutComponent.create(mContext).build();
Whitebox.setInternalState(content1, "mLifecycle", mockLifecycle1);
Whitebox.setInternalState(content2, "mLifecycle", mockLifecycle2);
final EventHandler visibleHandler1 = createEventHandler(content1, VISIBLE);
final EventHandler visibleHandler2 = createEventHandler(content2, VISIBLE);
final List<VisibilityOutput> visibilityOutputs = new ArrayList<>();
visibilityOutputs.add(createVisibilityOutput(
content1,
new Rect(LEFT, 5, RIGHT, 10),
visibleHandler1,
null,
null,
null,
null));
visibilityOutputs.add(createVisibilityOutput(
content2,
new Rect(LEFT, 10, RIGHT, 15),
visibleHandler2,
null,
null,
null,
null));
final LayoutState layoutState = new LayoutState();
Whitebox.setInternalState(layoutState, "mVisibilityOutputs", visibilityOutputs);
mMountState.mount(layoutState, new Rect(LEFT, 7, RIGHT, 12));
verify(mockLifecycle1, times(1)).dispatchOnEvent(
eq(visibleHandler1),
isA(VisibleEvent.class));
verify(mockLifecycle2, times(1)).dispatchOnEvent(
eq(visibleHandler2),
isA(VisibleEvent.class));
assertEquals(2, getVisibilityItemMapSize());
}
private int getVisibilityItemMapSize() {
return ((LongSparseArray) Whitebox.getInternalState(
mMountState,
"mVisibilityIdToItemMap")).size();
}
private static void checkNoVisibilityEventsDispatched(ComponentLifecycle lifecycle) {
verify(lifecycle, times(0)).dispatchOnEvent(isA(EventHandler.class), isA(VisibleEvent.class));
verify(lifecycle, times(0)).dispatchOnEvent(
isA(EventHandler.class),
isA(FocusedVisibleEvent.class));
verify(lifecycle, times(0)).dispatchOnEvent(
isA(EventHandler.class),
isA(FullImpressionVisibleEvent.class));
verify(lifecycle, times(0)).dispatchOnEvent(isA(EventHandler.class), isA(InvisibleEvent.class));
}
private static EventHandler createEventHandler(Component<?> component, int type) {
EventHandler handler = new EventHandler(component, type);
return handler;
}
private static ComponentLifecycle createLifecycleMock() {
ComponentLifecycle mock = mock(ComponentLifecycle.class);
doReturn(null).when(mock).dispatchOnEvent(any(EventHandler.class), any());
return mock;
}
private VisibilityOutput createVisibilityOutput(
Component<?> component,
Rect bounds,
EventHandler visibleHandler,
EventHandler focusedHandler,
EventHandler unfocusedHandler,
EventHandler fullImpressionHandler,
EventHandler invisibleHandler) {
VisibilityOutput visibilityOutput = new VisibilityOutput();
Whitebox.setInternalState(visibilityOutput, "mComponent", component);
visibilityOutput.setBounds(bounds);
visibilityOutput.setVisibleEventHandler(visibleHandler);
visibilityOutput.setFocusedEventHandler(focusedHandler);
visibilityOutput.setUnfocusedEventHandler(unfocusedHandler);
visibilityOutput.setFullImpressionEventHandler(fullImpressionHandler);
visibilityOutput.setInvisibleEventHandler(invisibleHandler);
mLastVisibilityOutputId += 1;
visibilityOutput.setId(mLastVisibilityOutputId);
return visibilityOutput;
}
}