ComponentKitTests/CKTreeNodeTests.mm (492 lines of code) (raw):
/*
* Copyright (c) 2014-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.
*
*/
#import <XCTest/XCTest.h>
#import <ComponentKit/CKBuildComponent.h>
#import <ComponentKit/CKComponentScopeRoot.h>
#import <ComponentKit/CKComponentScopeRootFactory.h>
#import <ComponentKit/CKFlexboxComponent.h>
#import <ComponentKit/CKComponent.h>
#import <ComponentKit/CKCompositeComponent.h>
#import <ComponentKit/CKRenderComponent.h>
#import <ComponentKit/CKLayoutComponent.h>
#import <ComponentKit/CKComponentInternal.h>
#import <ComponentKit/CKButtonComponent.h>
#import <ComponentKit/CKThreadLocalComponentScope.h>
#import <ComponentKit/CKBuildComponent.h>
#import "CKComponentTestCase.h"
static BOOL verifyChildToParentConnection(CKTreeNode *parentNode, CKTreeNode *childNode, id<CKRenderComponentProtocol> c) {
auto const componentKey = [childNode componentKey];
auto const childComponent = [parentNode childForComponentKey:componentKey].component;
return [childComponent isEqual:c];
}
static NSMutableArray<CKTreeNode*> *createsNodesForComponentsWithOwner(CKTreeNode *owner,
CKTreeNode *previousParent,
CKComponentScopeRoot *scopeRoot,
NSArray<id<CKRenderComponentProtocol>> *components) {
NSMutableArray<CKTreeNode*> *nodes = [NSMutableArray array];
for (id<CKRenderComponentProtocol> component in components) {
CKTreeNode *childNode = [CKTreeNode childPairForComponent:component
parent:owner
previousParent:previousParent
scopeRoot:scopeRoot
stateUpdates:{}].node;
[nodes addObject:childNode];
}
return nodes;
}
/** Iterate recursively over the tree and add its node identifiers to the set */
static void treeChildrenIdentifiers(CKTreeNode *node, NSMutableSet<NSString *> *identifiers, int level) {
for (auto childNode : node.children) {
// We add the child identifier + its level in the tree.
[identifiers addObject:[NSString stringWithFormat:@"%d-%d",childNode.nodeIdentifier, level]];
treeChildrenIdentifiers(childNode, identifiers, level+1);
}
}
/** Compare the children of the trees recursively; returns true if the two trees are equal */
static BOOL areTreesEqual(CKTreeNode *lhs, CKTreeNode *rhs) {
NSMutableSet<NSString *> *lhsChildrenIdentifiers = [NSMutableSet set];
treeChildrenIdentifiers(lhs, lhsChildrenIdentifiers, 0);
NSMutableSet<NSString *> *rhsChildrenIdentifiers = [NSMutableSet set];
treeChildrenIdentifiers(rhs, rhsChildrenIdentifiers, 0);
return [lhsChildrenIdentifiers isEqualToSet:rhsChildrenIdentifiers];
}
static CKComponent* buildComponent(CKComponent*(^block)()) {
__block CKComponent *c;
CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^CKComponent *{
c = block();
return c;
});
return c;
}
@interface CKTreeNodeTest_Component_WithScope : CKComponent
@end
@interface CKTreeNodeTest_RenderComponent_WithChild : CKRenderComponent
{
CKComponent *_childComponent;
}
+ (instancetype)newWithComponent:(CKComponent *)component;
@end
@interface CKTreeNodeTest_RenderComponent_NoInitialState : CKRenderComponent
@end
@interface CKTreeNodeTest_Component_WithState : CKComponent
@end
@interface CKTreeNodeTest_RenderComponent_WithState : CKRenderComponent
@end
@interface CKTreeNodeTest_RenderComponent_WithStateFromProps : CKRenderComponent
+ (instancetype)newWithProp:(id)prop;
@end
@interface CKTreeNodeTest_RenderComponent_WithNilState : CKRenderComponent
@end
@interface CKTreeNodeTest_RenderComponent_WithIdentifier : CKRenderComponent
+ (instancetype)newWithIdentifier:(id<NSObject>)identifier;
@end
@interface CKTreeNodeTests : CKComponentTestCase
@end
@implementation CKTreeNodeTests
#pragma mark - CKTreeNodeWithChildren
- (void)test_childForComponentKey_onCKTreeNodeWithChildren_withChild {
// Simulate first component tree creation
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const root1 = [CKTreeNode rootNode];
auto const component1 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode1 = [CKTreeNode childPairForComponent:component1
parent:root1
previousParent:nil
scopeRoot:scopeRoot
stateUpdates:{}].node;
// Simulate a component tree creation due to a state update
auto const root2 = [CKTreeNode rootNode];
auto const component2 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode2 = [CKTreeNode childPairForComponent:component2
parent:root2
previousParent:root1
scopeRoot:[scopeRoot newRoot]
stateUpdates:{}].node;
XCTAssertTrue(verifyChildToParentConnection(root1, childNode1, component1));
XCTAssertTrue(verifyChildToParentConnection(root2, childNode2, component2));
}
- (void)test_nodeIdentifier_onCKTreeNodeWithChildren_betweenGenerations_withChild {
// Simulate first component tree creation
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const root1 = [CKTreeNode rootNode];
auto const component1 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode1 = [CKTreeNode childPairForComponent:component1
parent:root1
previousParent:nil
scopeRoot:scopeRoot
stateUpdates:{}].node;
// Simulate a component tree creation due to a state update
auto const root2 = [CKTreeNode rootNode];
auto const component2 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode2 = [CKTreeNode childPairForComponent:component2
parent:root2
previousParent:root1
scopeRoot:[scopeRoot newRoot]
stateUpdates:{}].node;
XCTAssertEqual(childNode1.nodeIdentifier, childNode2.nodeIdentifier);
}
- (void)test_childForComponentKey_onCKTreeNodeWithChildren_withMultipleChildren {
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const root = [CKTreeNode rootNode];
// Create 4 children components
NSArray<id<CKRenderComponentProtocol>> *components = @[[CKTreeNodeTest_RenderComponent_NoInitialState new],
[CKTreeNodeTest_RenderComponent_NoInitialState new],
[CKTreeNodeTest_RenderComponent_WithState new],
[CKTreeNodeTest_RenderComponent_NoInitialState new]];
// Create a childNode for each.
NSMutableArray<CKTreeNode*> *nodes = createsNodesForComponentsWithOwner(root, nil, scopeRoot, components);
// Make sure the connections between the parent to the child nodes are correct
for (NSUInteger i=0; i<components.count; i++) {
CKTreeNode *childNode = nodes[i];
auto const component = components[i];
XCTAssertTrue(verifyChildToParentConnection(root, childNode, component));
}
// Create 4 children components
auto const root2 = [CKTreeNode rootNode];
NSArray<id<CKRenderComponentProtocol>> *components2 = @[[CKTreeNodeTest_RenderComponent_NoInitialState new],
[CKTreeNodeTest_RenderComponent_NoInitialState new],
[CKTreeNodeTest_RenderComponent_WithState new],
[CKTreeNodeTest_RenderComponent_NoInitialState new]];
__unused NSMutableArray<CKTreeNode*> *nodes2 = createsNodesForComponentsWithOwner(root2, root, [scopeRoot newRoot], components2);
// Verify that the two trees are equal.
XCTAssertTrue(areTreesEqual(root, root2));
}
- (void)test_childForComponentKey_onCKTreeNodeWithChildren_withDifferentChildOverGenerations
{
// Simulate first component tree creation
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const root1 = [CKTreeNode rootNode];
auto const component1 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode1 = [CKTreeNode childPairForComponent:component1
parent:root1
previousParent:nil
scopeRoot:scopeRoot
stateUpdates:{}].node;
// Simulate a component tree creation with a DIFFRENT child
auto const root2 = [CKTreeNode rootNode];
auto const component2 = [CKRenderComponent new];
CKTreeNode *childNode2 = [CKTreeNode childPairForComponent:component2
parent:root2
previousParent:root1
scopeRoot:[scopeRoot newRoot]
stateUpdates:{}].node;
XCTAssertTrue(verifyChildToParentConnection(root1, childNode1, component1));
XCTAssertTrue(verifyChildToParentConnection(root2, childNode2, component2));
XCTAssertNotEqual(childNode1.nodeIdentifier, childNode2.nodeIdentifier);
}
#pragma mark - State
- (void)test_stateUpdate_onCKTreeNode
{
// The 'resolve' method in CKComponentScopeHandle requires a CKThreadLocalComponentScope.
// We should get rid of this assert once we move to the render method only.
CKThreadLocalComponentScope threadScope(CKComponentScopeRootWithDefaultPredicates(nil, nil), {});
// Simulate first component tree creation
auto const root1 = [CKTreeNode rootNode];
auto const component1 = [CKTreeNodeTest_RenderComponent_WithState new];
CKTreeNode *childNode = [CKTreeNode childPairForComponent:component1
parent:root1
previousParent:nil
scopeRoot:threadScope.newScopeRoot
stateUpdates:{}].node;
// Verify that the initial state is correct.
XCTAssertTrue([childNode.state isEqualToNumber:[[component1 class] initialState]]);
// Simulate a component tree creation due to a state update
auto const root2 = [CKTreeNode rootNode];
auto const component2 = [CKTreeNodeTest_RenderComponent_WithState new];
// Simulate a state update
auto const newState = @2;
auto const scopeHandle = childNode.scopeHandle;
CKComponentStateUpdateMap stateUpdates;
stateUpdates[scopeHandle].push_back(^(id){
return newState;
});
CKTreeNode *childNode2 = [CKTreeNode childPairForComponent:component2
parent:root2
previousParent:root1
scopeRoot:[threadScope.newScopeRoot newRoot]
stateUpdates:stateUpdates].node;
XCTAssertTrue([childNode2.state isEqualToNumber:newState]);
}
- (void)test_nonNil_initialState_onCKTreeNode_withCKComponentSubclass
{
__block CKComponent *c;
buildComponent(^CKComponent*{
c = [CKTreeNodeTest_Component_WithState new];
// Using flexbox here to add a render component to the hierarchy, which forces buildComponentTree:
return CK::FlexboxComponentBuilder()
.child(c)
.child([CKTreeNodeTest_RenderComponent_WithNilState new])
.build();
});
[self _test_nonNil_initialState_withComponent:c];
}
- (void)test_emptyInitialState_onCKTreeNode_withCKComponentSubclass
{
auto const c = buildComponent(^{ return [CKComponent new]; });
[self _test_emptyInitialState_withComponent:c];
}
- (void)test_nonNil_initialState_onCKRenderTreeNode_withCKRenderComponent
{
auto const c = buildComponent(^{ return [CKTreeNodeTest_RenderComponent_WithState new]; });
[self _test_nonNil_initialState_withComponent:c];
}
- (void)test_emptyInitialState_onCKRenderTreeNode_withCKRenderComponent
{
auto const c = buildComponent(^{ return [CKTreeNodeTest_RenderComponent_NoInitialState new]; });
[self _test_emptyInitialState_withComponent:c];
}
- (void)test_initialStateFromProps_onCKRenderTreeNode_withCKRenderComponent
{
id prop = @1;
auto const c = buildComponent(^{ return [CKTreeNodeTest_RenderComponent_WithStateFromProps newWithProp:prop]; });
[self _test_initialState_withComponent:c initialState:prop];
}
- (void)test_nilInitialState_onCKRenderTreeNode_withCKRenderComponent
{
// Make sure CKRenderComponent supports nil initial state from prop.
id prop = nil;
auto const c = buildComponent(^{ return [CKTreeNodeTest_RenderComponent_WithStateFromProps newWithProp:prop]; });
[self _test_initialState_withComponent:c initialState:nil];
// Make sure CKRenderComponent supports nil initial.
auto const c2 = buildComponent(^{ return [CKTreeNodeTest_RenderComponent_WithNilState new]; });
[self _test_initialState_withComponent:c2 initialState:nil];
}
- (void)test_componentIdentifierOnCKTreeNodeWithChildren_withReorder {
// Simulate first component tree creation
__block CKTreeNodeTest_RenderComponent_WithIdentifier *c1;
__block CKTreeNodeTest_RenderComponent_WithIdentifier *c2;
auto const results = CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^CKComponent *{
c1 = [CKTreeNodeTest_RenderComponent_WithIdentifier newWithIdentifier:@1];
c2 = [CKTreeNodeTest_RenderComponent_WithIdentifier newWithIdentifier:@2];
return CK::FlexboxComponentBuilder()
.alignItems(CKFlexboxAlignItemsStretch)
.child(c1)
.child(c2)
.build();
});
// Simulate a props update which *reorders* the children.
__block CKTreeNodeTest_RenderComponent_WithIdentifier *c1SecondGen;
__block CKTreeNodeTest_RenderComponent_WithIdentifier *c2SecondGen;
auto const results2 = CKBuildComponent(results.scopeRoot, {}, ^CKComponent *{
c1SecondGen = [CKTreeNodeTest_RenderComponent_WithIdentifier newWithIdentifier:@1];
c2SecondGen = [CKTreeNodeTest_RenderComponent_WithIdentifier newWithIdentifier:@2];
return CK::FlexboxComponentBuilder()
.alignItems(CKFlexboxAlignItemsStretch)
.child(c2SecondGen)
.child(c1SecondGen)
.build();
});
// Make sure each component retreive its correct state even after reorder.
XCTAssertEqual(c1.treeNode.scopeHandle.state, c1SecondGen.treeNode.scopeHandle.state);
XCTAssertEqual(c2.treeNode.scopeHandle.state, c2SecondGen.treeNode.scopeHandle.state);
}
- (void)test_componentIdentifierOnCKTreeNodeWithChildren_withRemovingComponents {
// Simulate first component tree creation
__block CKTreeNodeTest_RenderComponent_WithIdentifier *c1;
__block CKTreeNodeTest_RenderComponent_WithIdentifier *c2;
__block CKTreeNodeTest_RenderComponent_WithIdentifier *c3;
auto const results = CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^CKComponent *{
c1 = [CKTreeNodeTest_RenderComponent_WithIdentifier newWithIdentifier:@1];
c2 = [CKTreeNodeTest_RenderComponent_WithIdentifier newWithIdentifier:@2];
c3 = [CKTreeNodeTest_RenderComponent_WithIdentifier newWithIdentifier:@3];
return CK::FlexboxComponentBuilder()
.alignItems(CKFlexboxAlignItemsStretch)
.child(c1)
.child(c2)
.child(c3)
.build();
});
// Simulate a props update which *removes* c2 from the hierarchy.
__block CKTreeNodeTest_RenderComponent_WithIdentifier *c1SecondGen;
__block CKTreeNodeTest_RenderComponent_WithIdentifier *c3SecondGen;
auto const results2 = CKBuildComponent(results.scopeRoot, {}, ^CKComponent *{
c1SecondGen = [CKTreeNodeTest_RenderComponent_WithIdentifier newWithIdentifier:@1];
c3SecondGen = [CKTreeNodeTest_RenderComponent_WithIdentifier newWithIdentifier:@3];
return CK::FlexboxComponentBuilder()
.alignItems(CKFlexboxAlignItemsStretch)
.child(c1SecondGen)
.child(c3SecondGen)
.build();
});
// Make sure each component retreive its correct state even after reorder.
XCTAssertEqual(c1.treeNode.scopeHandle.state, c1SecondGen.treeNode.scopeHandle.state);
XCTAssertEqual(c3.treeNode.scopeHandle.state, c3SecondGen.treeNode.scopeHandle.state);
}
#pragma mark - Helpers
- (void)_test_emptyInitialState_withComponent:(CKComponent *)c
{
XCTAssertNil(c.treeNode.scopeHandle.state);
}
- (void)_test_nonNil_initialState_withComponent:(CKComponent *)c
{
XCTAssertEqual([[c class] initialState], c.treeNode.scopeHandle.state);
XCTAssertNotNil(c.treeNode.scopeHandle);
}
- (void)_test_initialState_withComponent:(CKComponent *)c initialState:(id)initialState
{
XCTAssertEqual(initialState, c.treeNode.scopeHandle.state);
XCTAssertNotNil(c.treeNode.scopeHandle);
}
@end
@interface CKRenderTreeNodeTests : CKComponentTestCase
@end
@implementation CKRenderTreeNodeTests
#pragma mark - CKTreeNodeWithChild
- (void)test_childForComponentKey_onCKTreeNodeWithChild {
// Simulate first component tree creation
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
CKTreeNode *root1 = [CKTreeNode rootNode];
auto const component1 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode1 = [CKTreeNode childPairForComponent:component1
parent:root1
previousParent:nil
scopeRoot:scopeRoot
stateUpdates:{}].node;
// Simulate a component tree creation due to a state update
CKTreeNode *root2 = [CKTreeNode rootNode];
auto const component2 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode2 = [CKTreeNode childPairForComponent:component2
parent:root2
previousParent:root1
scopeRoot:[scopeRoot newRoot]
stateUpdates:{}].node;
XCTAssertTrue(verifyChildToParentConnection(root1, childNode1, component1));
XCTAssertTrue(verifyChildToParentConnection(root2, childNode2, component2));
}
- (void)test_nodeIdentifier_onCKTreeNodeWithChild_betweenGenerations {
// Simulate first component tree creation
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
CKTreeNode *root1 = [CKTreeNode rootNode];
auto const component1 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode1 = [CKTreeNode childPairForComponent:component1
parent:root1
previousParent:nil
scopeRoot:scopeRoot
stateUpdates:{}].node;
// Simulate a component tree creation due to a state update
CKTreeNode *root2 = [CKTreeNode rootNode];
auto const component2 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode2 = [CKTreeNode childPairForComponent:component2
parent:root2
previousParent:root1
scopeRoot:[scopeRoot newRoot]
stateUpdates:{}].node;
XCTAssertEqual(childNode1.nodeIdentifier, childNode2.nodeIdentifier);
}
- (void)test_childForComponentKey_onCKTreeNodeWithChild_withSameChildOverGenerations
{
// Simulate first component tree creation
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
CKTreeNode *root1 = [CKTreeNode rootNode];
auto const component1 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode1 = [CKTreeNode childPairForComponent:component1
parent:root1
previousParent:nil
scopeRoot:scopeRoot
stateUpdates:{}].node;
// Simulate a component tree creation due to a state update
CKTreeNode *root2 = [CKTreeNode rootNode];
auto const component2 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode2 = [CKTreeNode childPairForComponent:component2
parent:root2
previousParent:root1
scopeRoot:[scopeRoot newRoot]
stateUpdates:{}].node;
XCTAssertTrue(verifyChildToParentConnection(root1, childNode1, component1));
XCTAssertTrue(verifyChildToParentConnection(root2, childNode2, component2));
XCTAssertEqual(childNode1.nodeIdentifier, childNode2.nodeIdentifier);
}
- (void)test_childForComponentKey_onCKTreeNodeWithChild_withDifferentChildOverGenerations
{
// Simulate first component tree creation
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
CKTreeNode *root1 = [CKTreeNode rootNode];
auto const component1 = [CKTreeNodeTest_RenderComponent_NoInitialState new];
CKTreeNode *childNode1 = [CKTreeNode childPairForComponent:component1
parent:root1
previousParent:nil
scopeRoot:scopeRoot
stateUpdates:{}].node;
// Simulate a component tree creation with a DIFFRENT child
CKTreeNode *root2 = [CKTreeNode rootNode];
auto const component2 = [CKRenderComponent new];
CKTreeNode *childNode2 = [CKTreeNode childPairForComponent:component2
parent:root2
previousParent:root1
scopeRoot:[scopeRoot newRoot]
stateUpdates:{}].node;
XCTAssertTrue(verifyChildToParentConnection(root1, childNode1, component1));
XCTAssertTrue(verifyChildToParentConnection(root2, childNode2, component2));
XCTAssertNotEqual(childNode1.nodeIdentifier, childNode2.nodeIdentifier);
}
@end
#pragma mark - Helper Classes
@implementation CKTreeNodeTest_Component_WithState
+ (id)initialState
{
return @1;
}
+ (instancetype)new
{
CKComponentScope scope(self);
return [super new];
}
@end
@implementation CKTreeNodeTest_RenderComponent_WithState
+ (id)initialState
{
return @1;
}
- (CKComponent *)render:(id)state
{
return [CKComponent new];
}
@end
@implementation CKTreeNodeTest_RenderComponent_WithStateFromProps
{
id _prop;
}
+ (instancetype)newWithProp:(id)prop
{
auto const c = [super new];
if (c) {
c->_prop = prop;
}
return c;
}
- (id)initialState
{
return _prop;
}
- (CKComponent *)render:(id)state
{
return [CKComponent new];
}
@end
@implementation CKTreeNodeTest_RenderComponent_WithNilState
+ (id)initialState
{
return nil;
}
- (CKComponent *)render:(id)state
{
return [CKComponent new];
}
@end
@implementation CKTreeNodeTest_Component_WithScope
+ (instancetype)new
{
CKComponentScope scope(self);
return [super new];
}
@end
@implementation CKTreeNodeTest_RenderComponent_WithChild
+ (instancetype)newWithComponent:(CKComponent *)component
{
auto const c = [super new];
if (c) {
c->_childComponent = component;
}
return c;
}
+ (id)initialState
{
return nil;
}
- (CKComponent *)render:(id)state
{
return _childComponent;
}
@end
@implementation CKTreeNodeTest_RenderComponent_WithIdentifier
{
id<NSObject> _identifier;
}
+ (instancetype)newWithIdentifier:(id<NSObject>)identifier
{
auto const c = [super new];
if (c) {
c->_identifier = identifier;
}
return c;
}
- (id<NSObject>)componentIdentifier
{
return _identifier;
}
- (CKComponent *)render:(id)state
{
return [CKComponent new];
}
- (id)initialState
{
return _identifier;
}
@end
@implementation CKTreeNodeTest_RenderComponent_NoInitialState
- (CKComponent *)render:(id)state
{
return [CKComponent new];
}
@end