/** * 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 android.content.Context; import android.graphics.Rect; import android.view.ViewGroup; import com.facebook.litho.testing.ComponentTestHelper; import com.facebook.litho.testing.TestComponent; import com.facebook.litho.testing.TestComponentContextWithView; import com.facebook.litho.testing.TestDrawableComponent; import com.facebook.litho.testing.TestViewComponent; import com.facebook.litho.testing.testrunner.ComponentsTestRunner; import com.facebook.litho.testing.util.InlineLayoutSpec; import com.facebook.yoga.YogaAlign; import com.facebook.yoga.YogaEdge; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RuntimeEnvironment; import static com.facebook.litho.FrameworkLogEvents.EVENT_MOUNT; import static com.facebook.litho.FrameworkLogEvents.PARAM_MOUNTED_COUNT; import static com.facebook.litho.FrameworkLogEvents.PARAM_UNMOUNTED_COUNT; import static com.facebook.yoga.YogaEdge.ALL; import static com.facebook.yoga.YogaEdge.LEFT; import static com.facebook.yoga.YogaEdge.TOP; import static com.facebook.yoga.YogaPositionType.ABSOLUTE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(ComponentsTestRunner.class) public class MountStateIncrementalMountTest { private ComponentContext mContext; private ComponentsLogger mComponentsLogger; @Before public void setup() { mComponentsLogger = spy(new TestComponentsLogger()); when(mComponentsLogger.newEvent(any(int.class))).thenCallRealMethod(); when(mComponentsLogger.newPerformanceEvent(any(int.class))).thenCallRealMethod(); mContext = new ComponentContext(RuntimeEnvironment.application, "tag", mComponentsLogger); } /** * Tests incremental mount behaviour of a vertical stack of components with a View mount type. */ @Test public void testIncrementalMountVerticalViewStackScrollUp() { final TestComponent child1 = TestViewComponent.create(mContext) .build(); final TestComponent child2 = TestViewComponent.create(mContext) .build(); final LithoView lithoView = ComponentTestHelper.mountComponent( mContext, new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child( Layout.create(c, child1) .widthPx(10) .heightPx(10)) .child( Layout.create(c, child2) .widthPx(10) .heightPx(10)) .build(); } }); verifyLoggingAndResetLogger(2, 0); lithoView.getComponentTree().mountComponent(new Rect(0, -10, 10, -5)); assertFalse(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 2); lithoView.getComponentTree().mountComponent(new Rect(0, 0, 10, 5)); assertTrue(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(1, 0); lithoView.getComponentTree().mountComponent(new Rect(0, 5, 10, 15)); assertTrue(child1.isMounted()); assertTrue(child2.isMounted()); verifyLoggingAndResetLogger(1, 0); lithoView.getComponentTree().mountComponent(new Rect(0, 15, 10, 25)); assertFalse(child1.isMounted()); assertTrue(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); lithoView.getComponentTree().mountComponent(new Rect(0, 20, 10, 30)); assertFalse(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); } @Test public void testIncrementalMountVerticalViewStackScrollDown() { final TestComponent child1 = TestViewComponent.create(mContext) .build(); final TestComponent child2 = TestViewComponent.create(mContext) .build(); final LithoView lithoView = ComponentTestHelper.mountComponent( mContext, new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child( Layout.create(c, child1) .widthPx(10) .heightPx(10)) .child( Layout.create(c, child2) .widthPx(10) .heightPx(10)) .build(); } }); verifyLoggingAndResetLogger(2, 0); lithoView.getComponentTree().mountComponent(new Rect(0, 20, 10, 30)); assertFalse(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 2); lithoView.getComponentTree().mountComponent(new Rect(0, 15, 10, 25)); assertFalse(child1.isMounted()); assertTrue(child2.isMounted()); verifyLoggingAndResetLogger(1, 0); lithoView.getComponentTree().mountComponent(new Rect(0, 5, 10, 15)); assertTrue(child1.isMounted()); assertTrue(child2.isMounted()); verifyLoggingAndResetLogger(1, 0); lithoView.getComponentTree().mountComponent(new Rect(0, 0, 10, 10)); assertTrue(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); lithoView.getComponentTree().mountComponent(new Rect(0, -10, 10, -5)); assertFalse(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); } /** * Tests incremental mount behaviour of a horizontal stack of components with a View mount type. */ @Test public void testIncrementalMountHorizontalViewStack() { final TestComponent child1 = TestViewComponent.create(mContext) .build(); final TestComponent child2 = TestViewComponent.create(mContext) .build(); final LithoView lithoView = ComponentTestHelper.mountComponent( mContext, new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Row.create(c) .child( Layout.create(c, child1) .widthPx(10) .heightPx(10)) .child( Layout.create(c, child2) .widthPx(10) .heightPx(10)) .build(); } }); verifyLoggingAndResetLogger(2, 0); lithoView.getComponentTree().mountComponent(new Rect( -10, 0, -5, 10)); assertFalse(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 2); lithoView.getComponentTree().mountComponent(new Rect(0, 0, 5, 10)); assertTrue(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(1, 0); lithoView.getComponentTree().mountComponent(new Rect(5, 0, 15, 10)); assertTrue(child1.isMounted()); assertTrue(child2.isMounted()); verifyLoggingAndResetLogger(1, 0); lithoView.getComponentTree().mountComponent(new Rect(15, 0, 25, 10)); assertFalse(child1.isMounted()); assertTrue(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); lithoView.getComponentTree().mountComponent(new Rect(20, 0, 30, 10)); assertFalse(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); } /** * Tests incremental mount behaviour of a vertical stack of components with a Drawable mount type. */ @Test public void testIncrementalMountVerticalDrawableStack() { final TestComponent child1 = TestDrawableComponent.create(mContext) .build(); final TestComponent child2 = TestDrawableComponent.create(mContext) .build(); final LithoView lithoView = ComponentTestHelper.mountComponent( mContext, new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child( Layout.create(c, child1) .widthPx(10) .heightPx(10)) .child( Layout.create(c, child2) .widthPx(10) .heightPx(10)) .build(); } }); verifyLoggingAndResetLogger(2, 0); lithoView.getComponentTree().mountComponent(new Rect(0, -10, 10, -5)); assertFalse(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 2); lithoView.getComponentTree().mountComponent(new Rect(0, 0, 10, 5)); assertTrue(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(1, 0); lithoView.getComponentTree().mountComponent(new Rect(0, 5, 10, 15)); assertTrue(child1.isMounted()); assertTrue(child2.isMounted()); verifyLoggingAndResetLogger(1, 0); lithoView.getComponentTree().mountComponent(new Rect(0, 15, 10, 25)); assertFalse(child1.isMounted()); assertTrue(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); lithoView.getComponentTree().mountComponent(new Rect(0, 20, 10, 30)); assertFalse(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); } /** * Tests incremental mount behaviour of a view mount item in a nested hierarchy. */ @Test public void testIncrementalMountNestedView() { final TestComponent child = TestViewComponent.create(mContext) .build(); final LithoView lithoView = ComponentTestHelper.mountComponent( mContext, new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .wrapInView() .paddingPx(ALL, 20) .child( Layout.create(c, child) .widthPx(10) .heightPx(10)) .child(TestDrawableComponent.create(c)) .build(); } }); verifyLoggingAndResetLogger(2, 0); lithoView.getComponentTree().mountComponent(new Rect(0, 0, 50, 20)); assertFalse(child.isMounted()); verifyLoggingAndResetLogger(0, 2); lithoView.getComponentTree().mountComponent(new Rect(0, 0, 50, 40)); assertTrue(child.isMounted()); verifyLoggingAndResetLogger(2, 0); lithoView.getComponentTree().mountComponent(new Rect(30, 0, 50, 40)); assertFalse(child.isMounted()); verifyLoggingAndResetLogger(0, 1); } /** * Verify that we can cope with a negative padding on a component that is wrapped in a view * (since the bounds of the component will be larger than the bounds of the view). */ @Test public void testIncrementalMountVerticalDrawableStackNegativeMargin() { final TestComponent child1 = TestDrawableComponent.create(mContext) .build(); final LithoView lithoView = ComponentTestHelper.mountComponent( mContext, new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child( Layout.create(c, child1) .widthPx(10) .heightPx(10) .clickHandler(c.newEventHandler(1)) .marginDip(YogaEdge.TOP, -10)) .build(); } }); verifyLoggingAndResetLogger(2, 0); lithoView.getComponentTree().mountComponent(new Rect(0, -10, 10, -5)); verifyLoggingAndResetLogger(0, 0); } /** * Tests incremental mount behaviour of overlapping view mount items. */ @Test public void testIncrementalMountOverlappingView() { final TestComponent child1 = TestViewComponent.create(mContext) .build(); final TestComponent child2 = TestViewComponent.create(mContext) .build(); final LithoView lithoView = ComponentTestHelper.mountComponent( mContext, new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child( Layout.create(c, child1) .positionType(ABSOLUTE) .positionPx(TOP, 0) .positionPx(LEFT, 0) .widthPx(10) .heightPx(10)) .child( Layout.create(c, child2) .positionType(ABSOLUTE) .positionPx(TOP, 5) .positionPx(LEFT, 5) .widthPx(10) .heightPx(10)) .child(TestDrawableComponent.create(c)) .build(); } }); verifyLoggingAndResetLogger(3, 0); lithoView.getComponentTree().mountComponent(new Rect(0, 0, 5, 5)); assertTrue(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); lithoView.getComponentTree().mountComponent(new Rect(5, 5, 10, 10)); assertTrue(child1.isMounted()); assertTrue(child2.isMounted()); verifyLoggingAndResetLogger(1, 0); lithoView.getComponentTree().mountComponent(new Rect(10, 10, 15, 15)); assertFalse(child1.isMounted()); assertTrue(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); lithoView.getComponentTree().mountComponent(new Rect(15, 15, 20, 20)); assertFalse(child1.isMounted()); assertFalse(child2.isMounted()); verifyLoggingAndResetLogger(0, 1); } /** * Tests incremental mount behaviour of a child component that mounts incrementally. */ @Test public void testChildViewCanIncrementallyMount() { final TestLithoView mountedView = new TestLithoView(mContext); final TestComponentContextWithView testComponentContext = new TestComponentContextWithView(mContext, mountedView); final TestComponent child2 = TestViewComponent.create(testComponentContext).build(); final LithoView lithoView = ComponentTestHelper.mountComponent( testComponentContext, new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child( Layout.create(c, child2) .widthPx(10) .heightPx(20) .marginPx(YogaEdge.ALL, 2)) .build(); } }); for (int i = 0; i < 20; i++) { lithoView.getComponentTree().mountComponent(new Rect(0, 0, 10, 3 + i)); assertEquals(new Rect(0, 0, 8, 1 + i), mountedView.getPreviousIncrementalMountBounds()); } } @Test public void testChildLithoViewIncrementallyMounted() { final TestLithoView mountedView = new TestLithoView(mContext); mountedView.layout(0, 0, 100, 100); final TestComponentContextWithView testComponentContext = new TestComponentContextWithView(mContext, mountedView); final LithoView lithoView = ComponentTestHelper.mountComponent( TestViewComponent.create(testComponentContext)); assertTrue(mountedView.getPreviousIncrementalMountBounds().isEmpty()); lithoView.getComponentTree().mountComponent(new Rect(-10, -10, 10, 10)); assertEquals(new Rect(0, 0, 10, 10), mountedView.getPreviousIncrementalMountBounds()); lithoView.getComponentTree().mountComponent(new Rect(80, 80, 120, 120)); assertEquals(new Rect(80, 80, 100, 100), mountedView.getPreviousIncrementalMountBounds()); } @Test public void testChildViewGroupIncrementallyMounted() { final ViewGroup mountedView = mock(ViewGroup.class); when(mountedView.getLeft()).thenReturn(0); when(mountedView.getTop()).thenReturn(0); when(mountedView.getRight()).thenReturn(100); when(mountedView.getBottom()).thenReturn(100); when(mountedView.getChildCount()).thenReturn(3); final LithoView childView1 = getMockLithoViewWithBounds(new Rect(5, 10, 20, 30)); when(mountedView.getChildAt(0)).thenReturn(childView1); final LithoView childView2 = getMockLithoViewWithBounds(new Rect(10, 10, 50, 60)); when(mountedView.getChildAt(1)).thenReturn(childView2); final LithoView childView3 = getMockLithoViewWithBounds(new Rect(30, 35, 50, 60)); when(mountedView.getChildAt(2)).thenReturn(childView3); final TestComponentContextWithView testComponentContext = new TestComponentContextWithView(mContext, mountedView); final LithoView lithoView = ComponentTestHelper.mountComponent( TestViewComponent.create(testComponentContext)); // Can't verify directly as the object will have changed by the time we get the chance to // verify it. doAnswer( new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Rect rect = (Rect) invocation.getArguments()[0]; if (!rect.equals(new Rect(10, 5, 15, 20))) { fail(); } return null; } }).when(childView1).performIncrementalMount(any(Rect.class)); doAnswer( new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Rect rect = (Rect) invocation.getArguments()[0]; if (!rect.equals(new Rect(5, 5, 30, 30))) { fail(); } return null; } }).when(childView2).performIncrementalMount(any(Rect.class)); doAnswer( new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Rect rect = (Rect) invocation.getArguments()[0]; if (!rect.equals(new Rect(0, 0, 10, 5))) { fail(); } return null; } }).when(childView3).performIncrementalMount(any(Rect.class)); lithoView.getComponentTree().mountComponent(new Rect(15, 15, 40, 40)); verify(childView1).performIncrementalMount(any(Rect.class)); verify(childView2).performIncrementalMount(any(Rect.class)); verify(childView3).performIncrementalMount(any(Rect.class)); } /** * Tests incremental mount behaviour of a vertical stack of components with a View mount type. */ @Test public void testIncrementalMountDoesNotCauseMultipleUpdates() { final TestComponent child1 = TestViewComponent.create(mContext) .build(); final LithoView lithoView = ComponentTestHelper.mountComponent( mContext, new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return Column.create(c) .child( Layout.create(c, child1) .widthPx(10) .heightPx(10)) .build(); } }); lithoView.getComponentTree().mountComponent(new Rect(0, -10, 10, -5)); assertFalse(child1.isMounted()); assertTrue(child1.wasOnUnbindCalled()); assertTrue(child1.wasOnUnmountCalled()); lithoView.getComponentTree().mountComponent(new Rect(0, 0, 10, 5)); assertTrue(child1.isMounted()); child1.resetInteractions(); lithoView.getComponentTree().mountComponent(new Rect(0, 5, 10, 15)); assertTrue(child1.isMounted()); assertFalse(child1.wasOnBindCalled()); assertFalse(child1.wasOnMountCalled()); assertFalse(child1.wasOnUnbindCalled()); assertFalse(child1.wasOnUnmountCalled()); } private void verifyLoggingAndResetLogger(int mountedCount, int unmountedCount) { final LogEvent event = mComponentsLogger.newPerformanceEvent(EVENT_MOUNT); event.addParam(PARAM_MOUNTED_COUNT, String.valueOf(mountedCount)); event.addParam(PARAM_UNMOUNTED_COUNT, String.valueOf(unmountedCount)); verify(mComponentsLogger).log(eq(event)); reset(mComponentsLogger); } private static LithoView getMockLithoViewWithBounds(Rect bounds) { final LithoView lithoView = mock(LithoView.class); when(lithoView.getLeft()).thenReturn(bounds.left); when(lithoView.getTop()).thenReturn(bounds.top); when(lithoView.getRight()).thenReturn(bounds.right); when(lithoView.getBottom()).thenReturn(bounds.bottom); when(lithoView.getWidth()).thenReturn(bounds.width()); when(lithoView.getHeight()).thenReturn(bounds.height()); return lithoView; } private static class TestLithoView extends LithoView { private final Rect mPreviousIncrementalMountBounds = new Rect(); public TestLithoView(Context context) { super(context); } @Override public void performIncrementalMount(Rect visibleRect) { mPreviousIncrementalMountBounds.set(visibleRect); } private Rect getPreviousIncrementalMountBounds() { return mPreviousIncrementalMountBounds; } } }