/**
* 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.os.Looper;
import com.facebook.litho.testing.testrunner.ComponentsTestRunner;
import com.facebook.litho.testing.TestDrawableComponent;
import com.facebook.litho.testing.TestLayoutComponent;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.reflect.Whitebox;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.shadows.ShadowLooper;
import static com.facebook.litho.SizeSpec.AT_MOST;
import static com.facebook.litho.SizeSpec.EXACTLY;
import static com.facebook.litho.SizeSpec.makeSizeSpec;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.fail;
@RunWith(ComponentsTestRunner.class)
public class ComponentTreeTest {
private int mWidthSpec;
private int mWidthSpec2;
private int mHeightSpec;
private int mHeightSpec2;
private Component mComponent;
private ShadowLooper mLayoutThreadShadowLooper;
private ComponentContext mContext;
private static class TestComponent<L extends ComponentLifecycle> extends Component<L> {
public TestComponent(L component) {
super(component);
}
@Override
public String getSimpleName() {
return "TestComponent";
}
}
@Before
public void setup() throws Exception {
mContext = new ComponentContext(RuntimeEnvironment.application);
mComponent = TestDrawableComponent.create(mContext)
.build();
mLayoutThreadShadowLooper = Shadows.shadowOf(
(Looper) Whitebox.invokeMethod(
ComponentTree.class,
"getDefaultLayoutThreadLooper"));
mWidthSpec = makeSizeSpec(39, EXACTLY);
mWidthSpec2 = makeSizeSpec(40, EXACTLY);
mHeightSpec = makeSizeSpec(41, EXACTLY);
mHeightSpec2 = makeSizeSpec(42, EXACTLY);
}
private void creationCommonChecks(ComponentTree componentTree) {
// Not view or attached yet
Assert.assertNull(getLithoView(componentTree));
Assert.assertFalse(isAttached(componentTree));
// No measure spec from view yet.
Assert.assertFalse(
(Boolean) Whitebox.getInternalState(componentTree, "mHasViewMeasureSpec"));
// The component input should be the one we passed in
Assert.assertSame(
mComponent,
Whitebox.getInternalState(componentTree, "mRoot"));
}
private void postSizeSpecChecks(
ComponentTree componentTree,
String layoutStateVariableName) {
postSizeSpecChecks(
componentTree,
layoutStateVariableName,
mWidthSpec,
mHeightSpec);
}
private void postSizeSpecChecks(
ComponentTree componentTree,
String layoutStateVariableName,
int widthSpec,
int heightSpec) {
// Spec specified in create
Assert.assertTrue(componentTreeHasSizeSpec(componentTree));
assertEquals(
widthSpec,
Whitebox.getInternalState(componentTree, "mWidthSpec"));
assertEquals(
heightSpec,
Whitebox.getInternalState(componentTree, "mHeightSpec"));
LayoutState mainThreadLayoutState = Whitebox.getInternalState(
componentTree, "mMainThreadLayoutState");
LayoutState backgroundLayoutState = Whitebox.getInternalState(
componentTree, "mBackgroundLayoutState");
LayoutState layoutState = null;
LayoutState nullLayoutState = null;
if ("mMainThreadLayoutState".equals(layoutStateVariableName)) {
layoutState = mainThreadLayoutState;
nullLayoutState = backgroundLayoutState;
} else if ("mBackgroundLayoutState".equals(layoutStateVariableName)) {
layoutState = backgroundLayoutState;
nullLayoutState = mainThreadLayoutState;
} else {
fail("Incorrect variable name: " + layoutStateVariableName);
}
Assert.assertNull(nullLayoutState);
Assert.assertTrue(
layoutState.isCompatibleComponentAndSpec(
mComponent.getId(),
widthSpec,
heightSpec));
}
@Test
public void testCreate() {
ComponentTree componentTree =
ComponentTree.create(mContext, mComponent)
.incrementalMount(false)
.layoutDiffing(false)
.build();
creationCommonChecks(componentTree);
// Both the main thread and the background layout state shouldn't be calculated yet.
Assert.assertNull(Whitebox.getInternalState(componentTree, "mMainThreadLayoutState"));
Assert.assertNull(Whitebox.getInternalState(componentTree, "mBackgroundLayoutState"));
Assert.assertFalse(componentTreeHasSizeSpec(componentTree));
}
@Test
public void testSetSizeSpec() {
ComponentTree componentTree =
ComponentTree.create(mContext, mComponent)
.incrementalMount(false)
.layoutDiffing(false)
.build();
componentTree.setSizeSpec(mWidthSpec, mHeightSpec);
// Since this happens post creation, it's not in general safe to update the main thread layout
// state synchronously, so the result should be in the background layout state
postSizeSpecChecks(componentTree, "mBackgroundLayoutState");
}
@Test
public void testSetSizeSpecAsync() {
ComponentTree componentTree =
ComponentTree.create(mContext, mComponent)
.incrementalMount(false)
.layoutDiffing(false)
.build();
componentTree.setSizeSpecAsync(mWidthSpec, mHeightSpec);
// Only fields changed but no layout is done yet.
Assert.assertTrue(componentTreeHasSizeSpec(componentTree));
assertEquals(
mWidthSpec,
Whitebox.getInternalState(componentTree, "mWidthSpec"));
assertEquals(
mHeightSpec,
Whitebox.getInternalState(componentTree, "mHeightSpec"));
Assert.assertNull(Whitebox.getInternalState(componentTree, "mMainThreadLayoutState"));
Assert.assertNull(Whitebox.getInternalState(componentTree, "mBackgroundLayoutState"));
// Now the background thread run the queued task.
mLayoutThreadShadowLooper.runOneTask();
// Since this happens post creation, it's not in general safe to update the main thread layout
// state synchronously, so the result should be in the background layout state
postSizeSpecChecks(componentTree, "mBackgroundLayoutState");
}
@Test
public void testSetSizeSpecAsyncThenSyncBeforeRunningTask() {
ComponentTree componentTree =
ComponentTree.create(mContext, mComponent)
.incrementalMount(false)
.layoutDiffing(false)
.build();
componentTree.setSizeSpecAsync(mWidthSpec, mHeightSpec);
componentTree.setSizeSpec(mWidthSpec2, mHeightSpec2);
mLayoutThreadShadowLooper.runToEndOfTasks();
// Since this happens post creation, it's not in general safe to update the main thread layout
// state synchronously, so the result should be in the background layout state
postSizeSpecChecks(
componentTree,
"mBackgroundLayoutState",
mWidthSpec2,
mHeightSpec2);
}
@Test
public void testSetSizeSpecAsyncThenSyncAfterRunningTask() {
ComponentTree componentTree =
ComponentTree.create(mContext, mComponent)
.incrementalMount(false)
.layoutDiffing(false)
.build();
componentTree.setSizeSpecAsync(mWidthSpec, mHeightSpec);
mLayoutThreadShadowLooper.runToEndOfTasks();
componentTree.setSizeSpec(mWidthSpec2, mHeightSpec2);
// Since this happens post creation, it's not in general safe to update the main thread layout
// state synchronously, so the result should be in the background layout state
postSizeSpecChecks(
componentTree,
"mBackgroundLayoutState",
mWidthSpec2,
mHeightSpec2);
}
@Test
public void testSetSizeSpecWithOutput() {
ComponentTree componentTree =
ComponentTree.create(mContext, mComponent)
.incrementalMount(false)
.layoutDiffing(false)
.build();
Size size = new Size();
componentTree.setSizeSpec(mWidthSpec, mHeightSpec, size);
assertEquals(SizeSpec.getSize(mWidthSpec), size.width, 0.0);
assertEquals(SizeSpec.getSize(mHeightSpec), size.height, 0.0);
// Since this happens post creation, it's not in general safe to update the main thread layout
// state synchronously, so the result should be in the background layout state
postSizeSpecChecks(componentTree, "mBackgroundLayoutState");
}
@Test
public void testSetCompatibleSizeSpec() {
ComponentTree componentTree =
ComponentTree.create(mContext, mComponent)
.incrementalMount(false)
.layoutDiffing(false)
.build();
Size size = new Size();
componentTree.setSizeSpec(
SizeSpec.makeSizeSpec(100, AT_MOST),
SizeSpec.makeSizeSpec(100, AT_MOST),
size);
assertEquals(100, size.width, 0.0);
assertEquals(100, size.height, 0.0);
LayoutState firstLayoutState = componentTree.getBackgroundLayoutState();
assertNotNull(firstLayoutState);
componentTree.setSizeSpec(
SizeSpec.makeSizeSpec(100, EXACTLY),
SizeSpec.makeSizeSpec(100, EXACTLY),
size);
assertEquals(100, size.width, 0.0);
assertEquals(100, size.height, 0.0);
assertEquals(firstLayoutState, componentTree.getBackgroundLayoutState());
}
@Test
public void testSetCompatibleSizeSpecWithDifferentRoot() {
ComponentTree componentTree =
ComponentTree.create(mContext, mComponent)
.incrementalMount(false)
.layoutDiffing(false)
.build();
Size size = new Size();
componentTree.setSizeSpec(
SizeSpec.makeSizeSpec(100, AT_MOST),
SizeSpec.makeSizeSpec(100, AT_MOST),
size);
assertEquals(100, size.width, 0.0);
assertEquals(100, size.height, 0.0);
LayoutState firstLayoutState = componentTree.getBackgroundLayoutState();
assertNotNull(firstLayoutState);
componentTree.setRootAndSizeSpec(
TestDrawableComponent.create(mContext).build(),
SizeSpec.makeSizeSpec(100, EXACTLY),
SizeSpec.makeSizeSpec(100, EXACTLY),
size);
assertNotEquals(firstLayoutState, componentTree.getBackgroundLayoutState());
}
@Test
public void testSetInput() {
Component component = TestLayoutComponent.create(mContext)
.build();
ComponentTree componentTree =
ComponentTree.create(mContext, component)
.incrementalMount(false)
.layoutDiffing(false)
.build();
componentTree.setRoot(mComponent);
creationCommonChecks(componentTree);
Assert.assertNull(Whitebox.getInternalState(componentTree, "mMainThreadLayoutState"));
Assert.assertNull(Whitebox.getInternalState(componentTree, "mBackgroundLayoutState"));
componentTree.setSizeSpec(mWidthSpec, mHeightSpec);
// Since this happens post creation, it's not in general safe to update the main thread layout
// state synchronously, so the result should be in the background layout state
postSizeSpecChecks(componentTree, "mBackgroundLayoutState");
}
@Test
public void testSetComponentFromView() {
Component component1 = TestDrawableComponent.create(mContext)
.build();
ComponentTree componentTree1 = ComponentTree.create(
mContext,
component1)
.incrementalMount(false)
.layoutDiffing(false)
.build();
Component component2 = TestDrawableComponent.create(mContext)
.build();
ComponentTree componentTree2 = ComponentTree.create(
mContext,
component2)
.incrementalMount(false)
.layoutDiffing(false)
.build();
Assert.assertNull(getLithoView(componentTree1));
Assert.assertNull(getLithoView(componentTree2));
LithoView lithoView = new LithoView(mContext);
lithoView.setComponentTree(componentTree1);
Assert.assertNotNull(getLithoView(componentTree1));
Assert.assertNull(getLithoView(componentTree2));
lithoView.setComponentTree(componentTree2);
Assert.assertNull(getLithoView(componentTree1));
Assert.assertNotNull(getLithoView(componentTree2));
}
@Test
public void testComponentTreeReleaseClearsView() {
Component component = TestDrawableComponent.create(mContext)
.build();
ComponentTree componentTree = ComponentTree.create(
mContext,
component)
.incrementalMount(false)
.layoutDiffing(false)
.build();
LithoView lithoView = new LithoView(mContext);
lithoView.setComponentTree(componentTree);
assertEquals(lithoView.getComponentTree(), componentTree);
componentTree.release();
assertNull(lithoView.getComponentTree());
}
@Test
public void testsetTreeToTwoViewsBothAttached() {
Component component = TestDrawableComponent.create(mContext)
.build();
ComponentTree componentTree = ComponentTree.create(
mContext,
component)
.incrementalMount(false)
.layoutDiffing(false)
.build();
// Attach first view.
LithoView lithoView1 = new LithoView(mContext);
lithoView1.setComponentTree(componentTree);
lithoView1.onAttachedToWindow();
// Attach second view.
LithoView lithoView2 = new LithoView(mContext);
lithoView2.onAttachedToWindow();
// Set the component that is already mounted on the first view, on the second attached view.
// This should be ok.
lithoView2.setComponentTree(componentTree);
}
@Test
public void testSettingNewViewToTree() {
Component component = TestDrawableComponent.create(mContext)
.build();
ComponentTree componentTree = ComponentTree.create(
mContext,
component)
.incrementalMount(false)
.layoutDiffing(false)
.build();
// Attach first view.
LithoView lithoView1 = new LithoView(mContext);
lithoView1.setComponentTree(componentTree);
assertEquals(lithoView1, getLithoView(componentTree));
assertEquals(componentTree, getComponentTree(lithoView1));
// Attach second view.
LithoView lithoView2 = new LithoView(mContext);
Assert.assertNull(getComponentTree(lithoView2));
lithoView2.setComponentTree(componentTree);
assertEquals(lithoView2, getLithoView(componentTree));
assertEquals(componentTree, getComponentTree(lithoView2));
Assert.assertNull(getComponentTree(lithoView1));
}
private static LithoView getLithoView(ComponentTree componentTree) {
return Whitebox.getInternalState(componentTree, "mLithoView");
}
private static boolean isAttached(ComponentTree componentTree) {
return Whitebox.getInternalState(componentTree, "mIsAttached");
}
private static ComponentTree getComponentTree(LithoView lithoView) {
return Whitebox.getInternalState(lithoView, "mComponentTree");
}
private static boolean componentTreeHasSizeSpec(ComponentTree componentTree) {
try {
boolean hasCssSpec;
// Need to hold the lock on componentTree here otherwise the invocation of hasCssSpec
// will fail.
synchronized (componentTree) {
hasCssSpec = Whitebox.invokeMethod(componentTree, ComponentTree.class, "hasSizeSpec");
}
return hasCssSpec;
} catch (Exception e) {
throw new IllegalArgumentException("Failed to invoke hasSizeSpec on ComponentTree for: "+e);
}
}
}