/**
* 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.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import android.os.Looper;
import com.facebook.litho.ComponentLifecycle.StateContainer;
import com.facebook.litho.testing.ComponentTestHelper;
import com.facebook.litho.testing.testrunner.ComponentsTestRunner;
import com.facebook.litho.testing.util.InlineLayoutSpec;
import com.facebook.yoga.YogaAlign;
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.ComponentLifecycle.StateUpdate;
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;
@RunWith(ComponentsTestRunner.class)
public class StateUpdatesTest {
private static final int LIFECYCLE_TEST_ID = 1;
private static final int INITIAL_COUNT_STATE_VALUE = 4;
private int mWidthSpec;
private int mHeightSpec;
private final ComponentLifecycle mLifecycle = new ComponentLifecycle() {
@Override
int getId() {
return LIFECYCLE_TEST_ID;
}
@Override
protected boolean hasState() {
return true;
}
@Override
protected void createInitialState(ComponentContext c, Component component) {
TestComponent testComponent = (TestComponent) component;
testComponent.mStateContainer.mCount = INITIAL_COUNT_STATE_VALUE;
}
@Override
protected void transferState(
ComponentContext c,
StateContainer stateContainer,
Component component) {
TestStateContainer stateContainerImpl = (TestStateContainer) stateContainer;
TestComponent newTestComponent = (TestComponent) component;
newTestComponent.mStateContainer.mCount = stateContainerImpl.mCount;
}
};
private static class TestStateUpdate implements StateUpdate {
@Override
public void updateState(StateContainer stateContainer, Component component) {
TestStateContainer stateContainerImpl = (TestStateContainer) stateContainer;
TestComponent componentImpl = (TestComponent) component;
System.out.println("1 " + componentImpl.mStateContainer);
System.out.println("2 " + stateContainerImpl);
componentImpl.mStateContainer.mCount = stateContainerImpl.mCount + 1;
}
}
static class TestComponent<L extends ComponentLifecycle>
extends Component<L> implements Cloneable {
private TestStateContainer mStateContainer;
private TestComponent shallowCopy;
private int mId;
private static final AtomicInteger sIdGenerator = new AtomicInteger(0);
public TestComponent(L component) {
super(component);
mStateContainer = new TestStateContainer();
mId = sIdGenerator.getAndIncrement();
}
@Override
public String getSimpleName() {
return "TestComponent";
}
int getCount() {
return mStateContainer.mCount;
}
@Override
public Component makeShallowCopy() {
return this;
}
@Override
Component makeShallowCopyWithNewId() {
shallowCopy = (TestComponent) super.makeShallowCopy();
shallowCopy.mId = sIdGenerator.getAndIncrement();
return shallowCopy;
}
TestComponent getComponentForStateUpdate() {
if (shallowCopy == null) {
return this;
}
return shallowCopy.getComponentForStateUpdate();
}
@Override
protected int getId() {
return mId;
}
@Override
protected StateContainer getStateContainer() {
return mStateContainer;
}
}
static class TestStateContainer implements StateContainer {
protected int mCount;
}
private ShadowLooper mLayoutThreadShadowLooper;
private ComponentContext mContext;
private TestComponent mTestComponent;
private ComponentTree mComponentTree;
@Before
public void setup() throws Exception {
mContext = new ComponentContext(RuntimeEnvironment.application);
mWidthSpec = makeSizeSpec(39, EXACTLY);
mHeightSpec = makeSizeSpec(41, EXACTLY);
mLayoutThreadShadowLooper = Shadows.shadowOf(
(Looper) Whitebox.invokeMethod(
ComponentTree.class,
"getDefaultLayoutThreadLooper"));
mTestComponent = new TestComponent(mLifecycle);
mComponentTree = ComponentTree.create(mContext, mTestComponent)
.incrementalMount(false)
.layoutDiffing(false)
.build();
final LithoView lithoView = new LithoView(mContext);
lithoView.setComponentTree(mComponentTree);
lithoView.onAttachedToWindow();
ComponentTestHelper.measureAndLayout(lithoView);
}
@Test(expected = RuntimeException.class)
public void testCrashOnSameComponentKey() {
final Component child1 = new TestComponent(mLifecycle);
final Component child2 = new TestComponent(mLifecycle);
final Component component = new InlineLayoutSpec() {
@Override
protected ComponentLayout onCreateLayout(ComponentContext c) {
return Column.create(c)
.child(child1)
.child(child2)
.build();
}
};
final ComponentTree componentTree = ComponentTree.create(mContext, component)
.incrementalMount(false)
.layoutDiffing(false)
.build();
final LithoView lithoView = new LithoView(mContext);
lithoView.setComponentTree(componentTree);
lithoView.onAttachedToWindow();
ComponentTestHelper.measureAndLayout(lithoView);
}
@Test(expected = RuntimeException.class)
public void testCrashOnSameComponentKeyNestedContainers() {
final Component child1 = new TestComponent(mLifecycle);
final Component component = new InlineLayoutSpec() {
@Override
protected ComponentLayout onCreateLayout(ComponentContext c) {
return Column.create(c)
.child(
Column.create(c)
.child(child1))
.child(
Column.create(c)
.child(child1))
.build();
}
};
final ComponentTree componentTree = ComponentTree.create(mContext, component)
.incrementalMount(false)
.layoutDiffing(false)
.build();
final LithoView lithoView = new LithoView(mContext);
lithoView.setComponentTree(componentTree);
lithoView.onAttachedToWindow();
ComponentTestHelper.measureAndLayout(lithoView);
}
@Test
public void testKeepInitialStateValues() {
TestStateContainer previousStateContainer =
(TestStateContainer) getStateContainersMap().get(mTestComponent.getGlobalKey());
assertNotNull(previousStateContainer);
assertEquals(INITIAL_COUNT_STATE_VALUE, previousStateContainer.mCount);
}
@Test
public void testKeepUpdatedStateValue() {
mComponentTree.updateStateAsync(mTestComponent.getGlobalKey(), new TestStateUpdate());
mLayoutThreadShadowLooper.runOneTask();
TestStateContainer previousStateContainer =
(TestStateContainer) getStateContainersMap().get(mTestComponent.getGlobalKey());
assertNotNull(previousStateContainer);
assertEquals(INITIAL_COUNT_STATE_VALUE + 1, previousStateContainer.mCount);
}
@Test
public void testClearAppliedStateUpdates() {
mComponentTree.updateStateAsync(mTestComponent.getGlobalKey(), new TestStateUpdate());
assertEquals(1, getPendingStateUpdatesForComponent(mTestComponent).size());
mLayoutThreadShadowLooper.runOneTask();
assertNull(getPendingStateUpdatesForComponent(mTestComponent.getComponentForStateUpdate()));
}
@Test
public void testEnqueueStateUpdate() {
mComponentTree.updateStateAsync(mTestComponent.getGlobalKey(), new TestStateUpdate());
assertEquals(1, getPendingStateUpdatesForComponent(mTestComponent).size());
mLayoutThreadShadowLooper.runOneTask();
mComponentTree.updateStateAsync(mTestComponent.getGlobalKey(), new TestStateUpdate());
assertEquals(
INITIAL_COUNT_STATE_VALUE + 1,
((TestStateContainer) getStateContainersMap().get(mTestComponent.getGlobalKey())).mCount);
assertEquals(
1,
getPendingStateUpdatesForComponent(mTestComponent.getComponentForStateUpdate()).size());
}
@Test
public void testSetInitialStateValue() {
assertEquals(INITIAL_COUNT_STATE_VALUE, mTestComponent.getCount());
}
@Test
public void testUpdateState() {
mComponentTree.updateStateAsync(mTestComponent.getGlobalKey(), new TestStateUpdate());
mLayoutThreadShadowLooper.runOneTask();
assertEquals(
INITIAL_COUNT_STATE_VALUE + 1,
mTestComponent.getComponentForStateUpdate().getCount());
}
@Test
public void testTransferState() {
mComponentTree.updateStateAsync(mTestComponent.getGlobalKey(), new TestStateUpdate());
mLayoutThreadShadowLooper.runOneTask();
mComponentTree.setSizeSpec(mWidthSpec, mHeightSpec);
assertEquals(
INITIAL_COUNT_STATE_VALUE + 1,
mTestComponent.getComponentForStateUpdate().getCount());
}
@Test
public void testTransferAndUpdateState() {
mComponentTree.updateStateAsync(mTestComponent.getGlobalKey(), new TestStateUpdate());
mLayoutThreadShadowLooper.runOneTask();
mComponentTree.updateStateAsync(mTestComponent.getGlobalKey(), new TestStateUpdate());
mLayoutThreadShadowLooper.runOneTask();
assertEquals(
INITIAL_COUNT_STATE_VALUE + 2,
mTestComponent.getComponentForStateUpdate().getCount());
}
private StateHandler getStateHandler() {
return Whitebox.getInternalState(mComponentTree, "mStateHandler");
}
private Map<String, StateContainer> getStateContainersMap() {
return getStateHandler().getStateContainers();
}
private Map<String, List<StateUpdate>> getPendingStateUpdates() {
return getStateHandler().getPendingStateUpdates();
}
private List<StateUpdate> getPendingStateUpdatesForComponent(Component component) {
return getPendingStateUpdates().get(component.getGlobalKey());
}
}