/** * 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.res.Resources; import com.facebook.litho.testing.testrunner.ComponentsTestRunner; import com.facebook.litho.testing.util.InlineLayoutSpec; import com.facebook.yoga.YogaMeasureFunction; import com.facebook.yoga.YogaMeasureOutput; import com.facebook.yoga.YogaNode; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.rule.PowerMockRule; import org.powermock.reflect.Whitebox; import org.robolectric.RuntimeEnvironment; import static com.facebook.yoga.YogaMeasureMode.EXACTLY; import static junit.framework.Assert.fail; import static org.assertj.core.api.Java6Assertions.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; import static org.powermock.api.mockito.PowerMockito.mockStatic; /** * Tests {@link ComponentLifecycle} */ @PrepareForTest({ InternalNode.class, DiffNode.class, LayoutState.class, ComponentsPools.class}) @PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) @RunWith(ComponentsTestRunner.class) public class ComponentLifecycleTest { @Rule public PowerMockRule mPowerMockRule = new PowerMockRule(); private static final int A_HEIGHT = 11; private static final int A_WIDTH = 12; private int mNestedTreeWidthSpec; private int mNestedTreeHeightSpec; private InternalNode mNode; private DiffNode mDiffNode; private Component mInput; private ComponentContext mContext; private Component mComponentWithNullLayout; @Before public void setUp() { mDiffNode = mock(DiffNode.class); mNode = mock(InternalNode.class); final YogaNode cssNode = new YogaNode(); cssNode.setData(mNode); mNode.mYogaNode = cssNode; mockStatic(ComponentsPools.class); when(mNode.getLastWidthSpec()).thenReturn(-1); when(mNode.getDiffNode()).thenReturn(mDiffNode); when(mDiffNode.getLastMeasuredWidth()).thenReturn(-1f); when(mDiffNode.getLastMeasuredHeight()).thenReturn(-1f); when(ComponentsPools.acquireInternalNode(any(ComponentContext.class), any(Resources.class))) .thenReturn(mNode); mInput = mock(Component.class); mockStatic(LayoutState.class); mContext = new ComponentContext(RuntimeEnvironment.application); mComponentWithNullLayout = new InlineLayoutSpec() { @Override protected ComponentLayout onCreateLayout(ComponentContext c) { return null; } }; mNestedTreeWidthSpec = SizeSpec.makeSizeSpec(400, SizeSpec.EXACTLY); mNestedTreeHeightSpec = SizeSpec.makeSizeSpec(200, SizeSpec.EXACTLY); } @Test public void testCreateLayoutWithNullComponentWithLayoutSpecCannotMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( false /* isMountSpec */, false /* canMeasure */); componentLifecycle.createLayout(mContext, mComponentWithNullLayout, false); verify(componentLifecycle).onCreateLayout(mContext, mComponentWithNullLayout); verify(mNode).appendComponent(mComponentWithNullLayout); verify(componentLifecycle).onPrepare(mContext, mComponentWithNullLayout); } @Test public void testCreateLayoutWithNullComponentWithLayoutSpecCanMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( false /* isMountSpec */, true /* canMeasure */); componentLifecycle.createLayout(mContext, mComponentWithNullLayout, false); verify(componentLifecycle).onCreateLayout(mContext, mComponentWithNullLayout); verify(mNode).appendComponent(mComponentWithNullLayout); verify(componentLifecycle).onPrepare(mContext, mComponentWithNullLayout); } @Test public void testCreateLayoutWithNullComponentWithMountSpecCannotMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( true /* isMountSpec */, false /* canMeasure */); componentLifecycle.createLayout(mContext, mComponentWithNullLayout, false); verify(componentLifecycle).onCreateLayout(mContext, mComponentWithNullLayout); verify(mNode).appendComponent(mComponentWithNullLayout); verify(componentLifecycle).onPrepare(mContext, mComponentWithNullLayout); } @Test public void testCreateLayoutWithNullComponentWithMountSpecCanMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( true /* isMountSpec */, true /* canMeasure */); componentLifecycle.createLayout(mContext, mComponentWithNullLayout, false); verify(componentLifecycle).onCreateLayout(mContext, mComponentWithNullLayout); verify(mNode).appendComponent(mComponentWithNullLayout); verify(componentLifecycle).onPrepare(mContext, mComponentWithNullLayout); } @Test public void testCreateLayoutAndResolveNestedTreeWithMountSpecCannotMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( true /* isMountSpec */, false /* canMeasure */); componentLifecycle.createLayout(mContext, mInput, true); verify(componentLifecycle).onCreateLayout(mContext, mInput); verify(mNode).appendComponent(mInput); verify(mNode, never()).setMeasureFunction(any(YogaMeasureFunction.class)); verify(componentLifecycle).onPrepare(mContext, mInput); } @Test public void testCreateLayoutAndDontResolveNestedTreeWithMountSpecCannotMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( true /* isMountSpec */, false /* canMeasure */); componentLifecycle.createLayout(mContext, mInput, false); verify(componentLifecycle).onCreateLayout(mContext, mInput); verify(mNode).appendComponent(mInput); verify(mNode, never()).setMeasureFunction(any(YogaMeasureFunction.class)); verify(componentLifecycle).onPrepare(mContext, mInput); } @Test public void testCreateLayoutAndResolveNestedTreeWithMountSpecCanMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( true /* isMountSpec */, true /* canMeasure */); componentLifecycle.createLayout(mContext, mInput, true); verify(componentLifecycle).onCreateLayout(mContext, mInput); verify(mNode).appendComponent(mInput); verify(mNode).setMeasureFunction(any(YogaMeasureFunction.class)); verify(componentLifecycle).onPrepare(mContext, mInput); } @Test public void testCreateLayoutAndDontResolveNestedTreeWithMountSpecCanMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( true /* isMountSpec */, true /* canMeasure */); componentLifecycle.createLayout(mContext, mInput, false); verify(componentLifecycle).onCreateLayout(mContext, mInput); verify(mNode).appendComponent(mInput); verify(mNode).setMeasureFunction(any(YogaMeasureFunction.class)); verify(componentLifecycle).onPrepare(mContext, mInput); } @Test public void testCreateLayoutAndResolveNestedTreeWithLayoutSpecCannotMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( false /* isMountSpec */, false /* canMeasure */); componentLifecycle.createLayout(mContext, mInput, true); verify(componentLifecycle).onCreateLayout(mContext, mInput); verify(mNode).appendComponent(mInput); verify(mNode, never()).setMeasureFunction(any(YogaMeasureFunction.class)); verify(componentLifecycle).onPrepare(mContext, mInput); } @Test public void testCreateLayoutAndDontResolveNestedTreeWithLayoutSpecCannotMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( false /* isMountSpec */, false /* canMeasure */); componentLifecycle.createLayout(mContext, mInput, false); verify(componentLifecycle).onCreateLayout(mContext, mInput); verify(mNode).appendComponent(mInput); verify(mNode, never()).setMeasureFunction(any(YogaMeasureFunction.class)); verify(componentLifecycle).onPrepare(mContext, mInput); } @Test public void testCreateLayoutAndResolveNestedTreeWithLayoutSpecCanMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( false /* isMountSpec */, true /* canMeasure */); mContext.setWidthSpec(mNestedTreeWidthSpec); mContext.setHeightSpec(mNestedTreeHeightSpec); componentLifecycle.createLayout(mContext, mInput, true); verify(componentLifecycle).onCreateLayoutWithSizeSpec( mContext, mNestedTreeWidthSpec, mNestedTreeHeightSpec, mInput); verify(mNode).appendComponent(mInput); verify(mNode, never()).setMeasureFunction(any(YogaMeasureFunction.class)); verify(componentLifecycle).onPrepare(mContext, mInput); } @Test public void testCreateLayoutAndDontResolveNestedTreeWithLayoutSpecCanMeasure() { ComponentLifecycle componentLifecycle = setUpComponentForCreateLayout( false /* isMountSpec */, true /* canMeasure */); componentLifecycle.createLayout(mContext, mInput, false); PowerMockito.verifyStatic(); // Calling here to verify static call. ComponentsPools.acquireInternalNode(mContext, mContext.getResources()); verify(componentLifecycle, never()).onCreateLayout( any(ComponentContext.class), any(Component.class)); verify(componentLifecycle, never()).onCreateLayoutWithSizeSpec( any(ComponentContext.class), anyInt(), anyInt(), any(Component.class)); verify(mNode).appendComponent(mInput); verify(mNode).setMeasureFunction(any(YogaMeasureFunction.class)); verify(componentLifecycle, never()) .onPrepare(any(ComponentContext.class), any(Component.class)); } @Test public void testOnMeasureNotOverriden() { setUpComponentForCreateLayout(true, true); YogaMeasureFunction measureFunction = getMeasureFunction(); try { measureFunction.measure(mNode.mYogaNode, 0, EXACTLY, 0, EXACTLY); fail(); } catch (Exception e) { assertThat(e).isExactlyInstanceOf(IllegalStateException.class); assertThat(e.getMessage()).contains("canMeasure()"); } } @Test public void testMountSpecYogaMeasureOutputNotSet() { ComponentLifecycle componentLifecycle = new TestMountSpecWithEmptyOnMeasure(); YogaMeasureFunction measureFunction = getMeasureFunction(); when(mInput.getLifecycle()).thenReturn(componentLifecycle); Whitebox.setInternalState(mInput, "mLifecycle", componentLifecycle); try { measureFunction.measure(mNode.mYogaNode, 0, EXACTLY, 0, EXACTLY); fail(); } catch (Exception e) { assertThat(e).isExactlyInstanceOf(IllegalStateException.class); assertThat(e.getMessage()).contains("MeasureOutput not set"); } } @Test public void testMountSpecYogaMeasureOutputSet() { ComponentLifecycle componentLifecycle = new TestMountSpecSettingSizesInOnMeasure(); YogaMeasureFunction measureFunction = getMeasureFunction(); when(mInput.getLifecycle()).thenReturn(componentLifecycle); Whitebox.setInternalState(mInput, "mLifecycle", componentLifecycle); long output = measureFunction.measure(mNode.mYogaNode, 0, EXACTLY, 0, EXACTLY); assertThat(YogaMeasureOutput.getWidth(output)).isEqualTo(A_WIDTH); assertThat(YogaMeasureOutput.getHeight(output)).isEqualTo(A_HEIGHT); } @Test public void testLayoutSpecMeasureResolveNestedTree() { setUpComponentForCreateLayout(false /* isMountSpec */, true /* canMeasure */); YogaMeasureFunction measureFunction = getMeasureFunction(); final int nestedTreeWidth = 20; final int nestedTreeHeight = 25; InternalNode nestedTree = mock(InternalNode.class); when(nestedTree.getWidth()).thenReturn(nestedTreeWidth); when(nestedTree.getHeight()).thenReturn(nestedTreeHeight); when(LayoutState.resolveNestedTree(eq(mNode), anyInt(), anyInt())).thenReturn(nestedTree); long output = measureFunction.measure(mNode.mYogaNode, 0, EXACTLY, 0, EXACTLY); PowerMockito.verifyStatic(); LayoutState.resolveNestedTree(eq(mNode), anyInt(), anyInt()); assertThat(YogaMeasureOutput.getWidth(output)).isEqualTo(nestedTreeWidth); assertThat(YogaMeasureOutput.getHeight(output)).isEqualTo(nestedTreeHeight); } private ComponentLifecycle setUpComponentForCreateLayout( boolean isMountSpec, boolean canMeasure) { ComponentLifecycle componentLifecycle = spy(new TestBaseComponent()); when(componentLifecycle.canMeasure()).thenReturn(canMeasure); when(componentLifecycle.getMountType()).thenReturn( isMountSpec ? ComponentLifecycle.MountType.DRAWABLE : ComponentLifecycle.MountType.NONE); when(mInput.getLifecycle()).thenReturn(componentLifecycle); Whitebox.setInternalState(mInput, "mLifecycle", componentLifecycle); return componentLifecycle; } private YogaMeasureFunction getMeasureFunction() { when(mNode.getRootComponent()).thenReturn(mInput); return Whitebox.getInternalState(ComponentLifecycle.class, "sMeasureFunction"); } private class TestBaseComponent extends ComponentLifecycle { @Override protected ComponentLayout onCreateLayout(ComponentContext c, Component<?> input) { return mNode; } @Override protected ComponentLayout onCreateLayoutWithSizeSpec( ComponentContext c, int widthSpec, int heightSpec, Component<?> component) { return mNode; } } private class TestMountSpecWithEmptyOnMeasure extends TestBaseComponent { @Override public MountType getMountType() { return MountType.DRAWABLE; } @Override protected boolean canMeasure() { return true; } @Override protected void onMeasure( ComponentContext c, ComponentLayout layout, int widthSpec, int heightSpec, Size size, Component<?> component) { } } private class TestMountSpecSettingSizesInOnMeasure extends TestMountSpecWithEmptyOnMeasure { @Override protected void onMeasure( ComponentContext context, ComponentLayout layout, int widthSpec, int heightSpec, Size size, Component<?> component) { size.width = A_WIDTH; size.height = A_HEIGHT; } } }