ComponentKitTests/CKRenderComponentTests.mm (663 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/CKComponentInternal.h>
#import <ComponentKit/CKComponentSubclass.h>
#import <ComponentKit/CKFlexboxComponent.h>
#import <ComponentKit/CKThreadLocalComponentScope.h>
#import <ComponentKit/CKTreeNode.h>
#import <ComponentKit/CKComponentScopeRootFactory.h>
#import <ComponentKit/CKComponentScopeRoot.h>
#import <ComponentKit/CKRenderHelpers.h>
#import <ComponentKitTestHelpers/CKRenderComponentTestHelpers.h>
#import "CKComponentTestCase.h"
@interface CKRenderComponentTests : CKComponentTestCase
@end
@interface CKRenderComponentAndScopeTreeTests : CKComponentTestCase
@end
@implementation CKRenderComponentTests
{
CKTestRenderComponent *_c;
CKComponentScopeRoot *_scopeRoot;
@package
}
- (void)setUpForFasterStateUpdates
{
[self setUpForFasterStateUpdates:^{
return [CKTestRenderComponent new];
}];
}
- (void)setUpForFasterStateUpdates:(CKComponent *(^)(void))componentFactory
{
// New Tree Creation.
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
_c = (CKTestRenderComponent *)buildResults.component;
_scopeRoot = buildResults.scopeRoot;
XCTAssertEqual(_c.renderCalledCounter, 1);
}
- (void)setUpForFasterPropsUpdates:(CKComponent *(^)(void))componentFactory
{
// New Tree Creation.
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
_c = (CKTestRenderComponent *)buildResults.component;
_scopeRoot = buildResults.scopeRoot;
XCTAssertEqual(_c.renderCalledCounter, 1);
}
#pragma mark - State updates
- (void)test_fasterStateUpdate_componentIsBeingReused_onStateUpdateOnADifferentComponentBranch
{
[self setUpForFasterStateUpdates];
// Simulate a state update on a different component branch:
// 1. treeNodeDirtyIds with fake components ids.
// 2. parentHasStateUpdate = NO
CKThreadLocalComponentScope threadScope(_scopeRoot, {});
auto const c2 = [CKTestRenderComponent new];
CKComponentScopeRoot *scopeRoot2 = threadScope.newScopeRoot;
CKRender::ComponentTree::Root::build(c2, {
.scopeRoot = scopeRoot2,
.previousScopeRoot = _scopeRoot,
.stateUpdates = {},
.treeNodeDirtyIds = {100, 101}, // Use a random id that represents a fake state update on a different branch.
.buildTrigger = CKBuildTriggerStateUpdate,
});
// As the state update doesn't affect the c2, we should reuse c instead.
[self verifyComponentIsBeingReused:_c c2:c2 scopeRoot:_scopeRoot scopeRoot2:scopeRoot2];
}
- (void)test_fasterStateUpdate_componentIsNotBeingReused_onStateUpdateOnAParent
{
__block CKCompositeComponentWithScopeAndState *root;
__block CKTestRenderComponent *c;
auto const componentFactory = ^{
c = [CKTestRenderComponent newWithProps:{.identifier = 1}];
root = [CKCompositeComponentWithScopeAndState newWithComponent:c];
return root;
};
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Simulate a state update on a parent component:
// 1. parentHasStateUpdate = YES
// 2. treeNodeDirtyIds with a fake parent id.
CKComponentStateUpdateMap stateUpdates;
stateUpdates[root.treeNode.scopeHandle].push_back(^(id){
return @2;
});
__block CKCompositeComponentWithScopeAndState *root2;
__block CKTestRenderComponent *c2;
auto const componentFactory2 = ^{
c2 = [CKTestRenderComponent newWithProps:{.identifier = 2}];
root2 = [CKCompositeComponentWithScopeAndState newWithComponent:c2];
return root2;
};
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, NO);
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory2, buildTrigger2, CKReflowTriggerNone);
// c has dirty parent, however, the components are equal, so we reuse the previous component.
XCTAssertEqual(c.renderCalledCounter, 1);
XCTAssertEqual(c2.renderCalledCounter, 1);
XCTAssertFalse(c2.didReuseComponent);
XCTAssertNotEqual(c.childComponent, c2.childComponent);
// Make sure the dirtyParent is pssed correctly to the child component.
XCTAssertTrue(c2.childComponent.parentHasStateUpdate);
}
- (void)test_fasterStateUpdate_componentIsNotBeingReused_onStateUpdateOnTheComponent
{
[self setUpForFasterStateUpdates];
// Simulate a state update on the component itself:
// 1. treeNodeDirtyIds, contains the component tree node id.
// 2. stateUpdates, contains a state update block for the component.
CKThreadLocalComponentScope threadScope(_scopeRoot, {});
CKComponentScopeRoot *scopeRoot2 = threadScope.newScopeRoot;
CKComponentStateUpdateMap stateUpdates;
stateUpdates[_c.treeNode.scopeHandle] = {^id(id s) { return s; }};
auto const c2 = [CKTestRenderComponent new];
CKRender::ComponentTree::Root::build(c2, {
.scopeRoot = scopeRoot2,
.previousScopeRoot = _scopeRoot,
.stateUpdates = stateUpdates,
.treeNodeDirtyIds = {
_c.treeNode.nodeIdentifier
},
.buildTrigger = CKBuildTriggerStateUpdate,
});
// As the state update affect c2, we should recreate its children.
[self verifyComponentIsNotBeingReused:_c c2:c2 scopeRoot:_scopeRoot scopeRoot2:scopeRoot2];
// Make sure the dirtyParent is pssed correctly to the child component.
XCTAssertTrue(c2.childComponent.parentHasStateUpdate);
}
- (void)test_fasterStateUpdate_componentIsNotBeingReused_onStateUpdateOnItsChild
{
[self setUpForFasterStateUpdates];
// Simulate a state update on the component child:
// 1. treeNodeDirtyIds, contains the component tree node id.
// 2. stateUpdates, contains a state update block for the component.
// 3. hasDirtyParent = NO
CKThreadLocalComponentScope threadScope(_scopeRoot, {});
CKComponentScopeRoot *scopeRoot2 = threadScope.newScopeRoot;
CKComponentStateUpdateMap stateUpdates;
stateUpdates[_c.treeNode.scopeHandle] = {^id(id s) { return s; }};
auto const c2 = [CKTestRenderComponent new];
CKRender::ComponentTree::Root::build(c2, {
.scopeRoot = scopeRoot2,
.previousScopeRoot = _scopeRoot,
.stateUpdates = stateUpdates,
.treeNodeDirtyIds = {
_c.treeNode.nodeIdentifier,
},
.buildTrigger = CKBuildTriggerStateUpdate,
});
// As the state update affect c2, we should recreate its children.
[self verifyComponentIsNotBeingReused:_c c2:c2 scopeRoot:_scopeRoot scopeRoot2:scopeRoot2];
// Make sure the dirtyParent is pssed correctly to the child component.
XCTAssertTrue(c2.childComponent.parentHasStateUpdate);
}
#pragma mark - Faster Props Update
- (void)test_fasterPropsUpdate_componentIsNotBeingReused_whenPropsAreNotEqual
{
[self setUpForFasterPropsUpdates:^(){
return [CKTestRenderComponent newWithProps:{.identifier = 1}];
}];
// Simulate props update.
CKThreadLocalComponentScope threadScope(_scopeRoot, {});
auto const c2 = [CKTestRenderComponent newWithProps:{.identifier = 2}];
CKComponentScopeRoot *scopeRoot2 = threadScope.newScopeRoot;
CKRender::ComponentTree::Root::build(c2, {
.scopeRoot = scopeRoot2,
.previousScopeRoot = _scopeRoot,
.stateUpdates = {},
.treeNodeDirtyIds = {},
.buildTrigger = CKBuildTriggerPropsUpdate,
});
// Props are not equal, we cannot reuse the component in this case.
[self verifyComponentIsNotBeingReused:_c c2:c2 scopeRoot:_scopeRoot scopeRoot2:scopeRoot2];
// Make sure the dirtyParent is pssed correctly to the child component.
XCTAssertFalse(c2.childComponent.parentHasStateUpdate);
}
- (void)test_fasterPropsUpdate_componentIsBeingReusedWhenPropsAreEqual
{
// Use the same componentIdentifier for both components.
// shouldComponentUpdate: will return NO in this case and we can reuse the component.
NSUInteger componentIdentifier = 1;
[self setUpForFasterPropsUpdates:^{
return [CKTestRenderComponent newWithProps:{.identifier = componentIdentifier}];
}];
// Simulate props update.
CKThreadLocalComponentScope threadScope(_scopeRoot, {});
auto const c2 = [CKTestRenderComponent newWithProps:{.identifier = componentIdentifier}];
CKComponentScopeRoot *scopeRoot2 = threadScope.newScopeRoot;
CKRender::ComponentTree::Root::build(c2, {
.scopeRoot = scopeRoot2,
.previousScopeRoot = _scopeRoot,
.stateUpdates = {},
.treeNodeDirtyIds = {},
.buildTrigger = CKBuildTriggerPropsUpdate,
});
// The components are equal, we can reuse the previous component.
[self verifyComponentIsBeingReused:_c c2:c2 scopeRoot:_scopeRoot scopeRoot2:scopeRoot2];
// Make sure the dirtyParent is pssed correctly to the child component.
XCTAssertFalse(c2.childComponent.parentHasStateUpdate);
}
- (void)test_fasterPropsUpdate_componentIsBeingReused_onStateUpdateWithDirtyParentAndEqualComponents
{
__block CKCompositeComponentWithScopeAndState *root;
__block CKTestRenderComponent *c;
auto const componentIdentifier = 1;
auto const componentFactory = ^{
c = [CKTestRenderComponent newWithProps:{.identifier = componentIdentifier}];
root = [CKCompositeComponentWithScopeAndState newWithComponent:c];
return root;
};
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Simulate a state update on a parent component:
// 1. parentHasStateUpdate = YES
// 2. treeNodeDirtyIds with a fake parent id.
CKComponentStateUpdateMap stateUpdates;
stateUpdates[root.treeNode.scopeHandle].push_back(^(id){
return @2;
});
__block CKCompositeComponentWithScopeAndState *root2;
__block CKTestRenderComponent *c2;
auto const componentFactory2 = ^{
c2 = [CKTestRenderComponent newWithProps:{.identifier = componentIdentifier}];
root2 = [CKCompositeComponentWithScopeAndState newWithComponent:c2];
return root2;
};
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, NO);
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory2, buildTrigger2, CKReflowTriggerNone);
// c has dirty parent, however, the components are equal, so we reuse the previous component.
XCTAssertEqual(c.renderCalledCounter, 1);
XCTAssertEqual(c2.renderCalledCounter, 0);
XCTAssertTrue(c2.didReuseComponent);
XCTAssertEqual(c.childComponent, c2.childComponent);
}
- (void)test_fasterPropsUpdate_componentIsBeingReused_onStateUpdateWithNonDirtyComponentAndEqualComponents
{
auto const componentIdentifier = 1;
[self setUpForFasterPropsUpdates:^{
return [CKTestRenderComponent newWithProps:{.identifier = componentIdentifier}];
}];
// Simulate a state update on a parent component:
// 1. parentHasStateUpdate = NO
// 2. treeNodeDirtyIds is empty.
CKThreadLocalComponentScope threadScope(_scopeRoot, {});
auto const c2 = [CKTestRenderComponent newWithProps:{.identifier = componentIdentifier}];
CKComponentScopeRoot *scopeRoot2 = threadScope.newScopeRoot;
CKRender::ComponentTree::Root::build(c2, {
.scopeRoot = scopeRoot2,
.previousScopeRoot = _scopeRoot,
.stateUpdates = {},
.treeNodeDirtyIds = {},
.buildTrigger = CKBuildTriggerStateUpdate,
});
// c is not dirty, the components are equal, so we reuse the previous component.
[self verifyComponentIsBeingReused:_c c2:c2 scopeRoot:_scopeRoot scopeRoot2:scopeRoot2];
}
- (void)test_fasterPropsUpdate_componentIsNotBeingReusedWhenPropsAreEqualButNodeIdIsDirty
{
// Use the same componentIdentifier for both components.
// shouldComponentUpdate: will return NO in this case.
// However, we simulate a case when the node id is dirty, hence cannot be reused.
NSUInteger componentIdentifier = 1;
[self setUpForFasterPropsUpdates:^{
return [CKTestRenderComponent newWithProps:{
.identifier = componentIdentifier,
.shouldUseComponentContext = YES,
}];
}];
// Simulate props update.
__block CKTestRenderComponent *c2;
auto const componentFactory = ^{
c2 = [CKTestRenderComponent newWithProps:{
.identifier = componentIdentifier,
.shouldUseComponentContext = YES,
}];
return c2;
};
auto const scopeRoot = CK::makeNonNull(_scopeRoot);
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// The components are equal, but the node id is dirty - hence, we cannot reuse the previous component.
[self verifyComponentIsNotBeingReused:_c c2:c2 scopeRoot:_scopeRoot scopeRoot2:buildResults.scopeRoot];
}
#pragma mark - Props and State Update
- (void)test_fasterStateUpdate_componentIsNotBeingReused_onPropsAndStateUpdate
{
__block CKCompositeComponentWithScopeAndState *root;
__block CKTestRenderComponent *c;
auto const componentIdentifier = 1;
auto const componentFactory = ^{
c = [CKTestRenderComponent newWithProps:{.identifier = componentIdentifier}];
root = [CKCompositeComponentWithScopeAndState newWithComponent:c];
return root;
};
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Simulate props and a state update on a component
// 1. parentHasStateUpdate = YES
// 2. treeNodeDirtyIds with a fake parent id.
// 3. forcePropsUpdates = YES
// 4. Props (componentIdentifier) are equal = reuse
CKComponentStateUpdateMap stateUpdates;
stateUpdates[root.treeNode.scopeHandle].push_back(^(id){
return @2;
});
__block CKCompositeComponentWithScopeAndState *root2;
__block CKTestRenderComponent *c2;
auto const componentFactory2 = ^{
c2 = [CKTestRenderComponent newWithProps:{.identifier = componentIdentifier}];
root2 = [CKCompositeComponentWithScopeAndState newWithComponent:c2];
return root2;
};
auto const forcePropsUpdates = YES;
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, forcePropsUpdates);
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory2, buildTrigger2, CKReflowTriggerNone);
// forcePropsUpdates + state update should end up with PropsAndStateUpdate trigger - which behaves the same as PropsUpdates
XCTAssertEqual(c.renderCalledCounter, 1);
XCTAssertEqual(c2.renderCalledCounter, 0);
XCTAssertFalse(c.didReuseComponent);
XCTAssertTrue(c2.didReuseComponent);
XCTAssertEqual(c.childComponent, c2.childComponent);
}
- (void)test_fasterStateUpdate_componentIsBeingReused_onPropsAndStateUpdate
{
__block CKCompositeComponentWithScopeAndState *root;
__block CKTestRenderComponent *c;
auto const componentIdentifier = 1;
auto const componentFactory = ^{
c = [CKTestRenderComponent newWithProps:{.identifier = componentIdentifier}];
root = [CKCompositeComponentWithScopeAndState newWithComponent:c];
return root;
};
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Simulate props and a state update on a component
// 1. parentHasStateUpdate = YES
// 2. treeNodeDirtyIds with a fake parent id.
// 3. forcePropsUpdates = YES
// 4. Props (componentIdentifier) are different = no reuse
CKComponentStateUpdateMap stateUpdates;
stateUpdates[root.treeNode.scopeHandle].push_back(^(id){
return @2;
});
__block CKCompositeComponentWithScopeAndState *root2;
__block CKTestRenderComponent *c2;
// Simulate props update
auto const componentIdentifier2 = 2;
auto const componentFactory2 = ^{
c2 = [CKTestRenderComponent newWithProps:{.identifier = componentIdentifier2}];
root2 = [CKCompositeComponentWithScopeAndState newWithComponent:c2];
return root2;
};
auto const forcePropsUpdates = YES;
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, forcePropsUpdates);
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory2, buildTrigger2, CKReflowTriggerNone);
// forcePropsUpdates + state update should end up with PropsAndStateUpdate trigger - which behaves the same as PropsUpdates
XCTAssertEqual(c.renderCalledCounter, 1);
XCTAssertEqual(c2.renderCalledCounter, 1);
XCTAssertFalse(c.didReuseComponent);
XCTAssertFalse(c2.didReuseComponent);
XCTAssertNotEqual(c.childComponent, c2.childComponent);
}
#pragma mark - parentHasStateUpdate
- (void)test_parentHasStateUpdatePropagatedCorrectly
{
// Build new tree
__block CKTestRenderComponent *c;
__block CKCompositeComponentWithScopeAndState *rootComponent;
auto const componentFactory = ^{
c = [CKTestRenderComponent newWithProps:{.identifier = 1}];
rootComponent = generateComponentHierarchyWithComponent(c);
return rootComponent;
};
auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil);
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
XCTAssertFalse(c.childComponent.parentHasStateUpdate);
// Simulate a state update on the root.
CKComponentStateUpdateMap stateUpdates;
stateUpdates[rootComponent.treeNode.scopeHandle].push_back(^(id){
return @2;
});
__block CKTestRenderComponent *c2;
__block CKCompositeComponentWithScopeAndState *rootComponent2;
auto const componentFactory2 = ^{
// Use different identifier for c2 to make sure we don't reuse it (otherwise the buildComponentTree won't be called on the child component).
c2 = [CKTestRenderComponent newWithProps:{.identifier = 2}];
rootComponent2 = generateComponentHierarchyWithComponent(c2);
return rootComponent2;
};
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, NO);
CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory2, buildTrigger2, CKReflowTriggerNone);
XCTAssertTrue(c2.childComponent.parentHasStateUpdate);
}
- (void)test_registerComponentsAndControllersInScopeRootAfterReuse
{
// Build new tree with siblings `CKTestRenderComponent` components.
__block CKTestRenderComponent *c1;
__block CKTestRenderComponent *c2;
auto const componentFactory = ^{
c1 = [CKTestRenderComponent newWithProps:{.identifier = 1}];
c2 = [CKTestRenderComponent newWithProps:{.identifier = 2}];
return [CKTestLayoutComponent newWithChildren:{c1, c2}];
};
// Build scope root with predicates.
auto const scopeRoot = CKComponentScopeRootWithPredicates(nil, nil,
{&CKComponentRenderTestsPredicate},
{&CKComponentControllerRenderTestsPredicate});
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Verify components and controllers registration in the scope root.
[self verifyComponentsAndControllersAreRegisteredInScopeRoot:buildResults.scopeRoot
components:{c1.childComponent, c2.childComponent}
componentPredicate:&CKComponentRenderTestsPredicate
componentControllerPredicate:&CKComponentControllerRenderTestsPredicate];
// Simulate a state update on c2.
CKComponentStateUpdateMap stateUpdates;
stateUpdates[c2.treeNode.scopeHandle].push_back(^(id){
return @2;
});
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, NO);
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory, buildTrigger2, CKReflowTriggerNone);
// Verify c1 has been reused
XCTAssertTrue(c1.didReuseComponent);
// Verify components and controllers registration in the scope root.
[self verifyComponentsAndControllersAreRegisteredInScopeRoot:buildResults2.scopeRoot
components:{c1.childComponent,c2.childComponent}
componentPredicate:&CKComponentRenderTestsPredicate
componentControllerPredicate:&CKComponentControllerRenderTestsPredicate];
}
- (void)test_registerComponentsAndControllersInScopeRootAfterReuseWithNonRenderComponents
{
// Build new tree with siblings `CKTestRenderComponent` components.
__block CKTestRenderComponent *c1;
__block CKTestRenderComponent *c2;
auto const componentFactory = ^{
c1 = [CKTestRenderComponent newWithProps:{.identifier = 1}];
c2 = [CKTestRenderComponent newWithProps:{.identifier = 2, .shouldUseNonRenderChild = YES}];
return [CKTestLayoutComponent newWithChildren:{c1, c2}];
};
// Build scope root with predicates.
auto const scopeRoot = CKComponentScopeRootWithPredicates(nil, nil,
{&CKAllNonNilComponentsTestsPredicate},
{&CKAllNonNilComponentsControllersTestsPredicate});
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Verify components and controllers registration in the scope root.
[self verifyComponentsAndControllersAreRegisteredInScopeRoot:buildResults.scopeRoot
components:{
c1, c1.childComponent,
c2, c2.nonRenderChildComponent, c2.nonRenderChildComponent.childComponent,
}
componentPredicate:&CKAllNonNilComponentsTestsPredicate
componentControllerPredicate:&CKAllNonNilComponentsControllersTestsPredicate];
// Simulate a state update on c1.
CKComponentStateUpdateMap stateUpdates;
stateUpdates[c1.treeNode.scopeHandle].push_back(^(id){
return @2;
});
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, NO);
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory, buildTrigger2, CKReflowTriggerNone);
// Verify c1 has been reused
XCTAssertTrue(c2.didReuseComponent);
// Verify components and controllers registration in the scope root.
[self verifyComponentsAndControllersAreRegisteredInScopeRoot:buildResults2.scopeRoot
components:{
c1, c1.childComponent,
c2, c2.nonRenderChildComponent, c2.nonRenderChildComponent.childComponent,
}
componentPredicate:&CKAllNonNilComponentsTestsPredicate
componentControllerPredicate:&CKAllNonNilComponentsControllersTestsPredicate];
}
- (void)test_componentIsNotBeingReusedOnAStateUpdate_WhenEnableComponentReuseOptimizationsIsOff
{
// Build new tree with siblings `CKTestRenderComponent` components.
__block CKTestRenderComponent *c1;
__block CKTestRenderComponent *c2;
auto const componentFactory = ^{
c1 = [CKTestRenderComponent newWithProps:{.identifier = 1}];
c2 = [CKTestRenderComponent newWithProps:{.identifier = 2}];
return [CKTestLayoutComponent newWithChildren:{c1, c2}];
};
// Build scope root with predicates.
auto const scopeRoot = CKComponentScopeRootWithPredicates(nil, nil, {}, {});
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Simulate a state update on c2.
CKComponentStateUpdateMap stateUpdates;
stateUpdates[c2.treeNode.scopeHandle].push_back(^(id){
return @2;
});
auto ignoreComponentReuseOptimizations = YES;
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, ignoreComponentReuseOptimizations, NO);
auto const reflowTrigger2 = !ignoreComponentReuseOptimizations ? CKReflowTriggerNone : CKReflowTriggerReload;
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory, buildTrigger2, reflowTrigger2);
// Verify no component have been reused.
XCTAssertFalse(c1.didReuseComponent);
XCTAssertFalse(c2.didReuseComponent);
}
- (void)test_componentIsNotBeingReusedOnAPropsUpdate_WhenEnableComponentReuseOptimizationsIsOff
{
// Build new tree with siblings `CKTestRenderComponent` components.
__block CKTestRenderComponent *c1;
__block CKTestRenderComponent *c2;
auto const componentFactory = ^{
c1 = [CKTestRenderComponent newWithProps:{.identifier = 1}];
c2 = [CKTestRenderComponent newWithProps:{.identifier = 2}];
return [CKTestLayoutComponent newWithChildren:{c1, c2}];
};
// Build scope root with predicates.
auto const scopeRoot = CKComponentScopeRootWithPredicates(nil, nil, {}, {});
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
auto ignoreComponentReuseOptimizations = YES;
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, {}, ignoreComponentReuseOptimizations, NO);
auto const reflowTrigger2 = !ignoreComponentReuseOptimizations ? CKReflowTriggerNone : CKReflowTriggerReload;
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, {}, componentFactory, buildTrigger2, reflowTrigger2);
// Verify no component have been reused.
XCTAssertFalse(c1.didReuseComponent);
XCTAssertFalse(c2.didReuseComponent);
}
#pragma mark - Helpers
// Filters `CKTestChildRenderComponent` components.
static BOOL CKComponentRenderTestsPredicate(id<CKComponentProtocol> component) {
return [component class] == [CKTestChildRenderComponent class];
}
// Filters `CKTestChildRenderComponentController` controllers.
static BOOL CKComponentControllerRenderTestsPredicate(id<CKComponentControllerProtocol> controller) {
return [controller class] == [CKTestChildRenderComponentController class];
}
// Filters all non-nil components.
static BOOL CKAllNonNilComponentsTestsPredicate(id<CKComponentProtocol> component) {
return component != nil;
}
// Filters all non-nil controllers.
static BOOL CKAllNonNilComponentsControllersTestsPredicate(id<CKComponentControllerProtocol> controller) {
return controller != nil;
}
- (void)verifyComponentIsNotBeingReused:(CKTestRenderComponent *)c
c2:(CKTestRenderComponent *)c2
scopeRoot:(CKComponentScopeRoot *)scopeRoot
scopeRoot2:(CKComponentScopeRoot *)scopeRoot2
{
XCTAssertEqual(c.renderCalledCounter, 1);
XCTAssertEqual(c2.renderCalledCounter, 1);
XCTAssertFalse(c2.didReuseComponent);
XCTAssertNotEqual(c.childComponent, c2.childComponent);
}
- (void)verifyComponentIsBeingReused:(CKTestRenderComponent *)c
c2:(CKTestRenderComponent *)c2
scopeRoot:(CKComponentScopeRoot *)scopeRoot
scopeRoot2:(CKComponentScopeRoot *)scopeRoot2
{
XCTAssertEqual(c.renderCalledCounter, 1);
XCTAssertEqual(c2.renderCalledCounter, 0);
XCTAssertTrue(c2.didReuseComponent);
XCTAssertEqual(c.childComponent, c2.childComponent);
}
- (void)verifyComponentsAndControllersAreRegisteredInScopeRoot:(CKComponentScopeRoot *)scopeRoot
components:(std::vector<CKComponent *>)components
componentPredicate:(CKComponentPredicate)componentPredicate
componentControllerPredicate:(CKComponentControllerPredicate)componentControllerPredicate
{
auto const registeredComponents = [scopeRoot componentsMatchingPredicate:componentPredicate];
auto const registeredControllers = [scopeRoot componentControllersMatchingPredicate:componentControllerPredicate];
for (auto const &c: components) {
XCTAssertNotNil(c, @"component shoudn't be nil here");
XCTAssertTrue(CK::Collection::contains(registeredComponents, c));
if (c.controller) {
XCTAssertTrue(CK::Collection::contains(registeredControllers, c.controller));
}
}
}
static CKCompositeComponentWithScopeAndState* generateComponentHierarchyWithComponent(CKComponent *c) {
return
[CKCompositeComponentWithScopeAndState
newWithComponent:
CK::FlexboxComponentBuilder()
.child(c)
.build()];
}
@end
@implementation CKRenderComponentAndScopeTreeTests
- (void)test_scopeFramePreserveStateDuringComponentReuse
{
// Build new tree with siblings `CKTestRenderWithNonRenderWithStateChildComponent` components.
// Each `CKTestRenderWithNonRenderWithStateChildComponent` has non-render component with state.
__block CKTestRenderWithNonRenderWithStateChildComponent *c1;
__block CKTestRenderWithNonRenderWithStateChildComponent *c2;
auto const componentFactory = ^{
c1 = [CKTestRenderWithNonRenderWithStateChildComponent new];
c2 = [CKTestRenderWithNonRenderWithStateChildComponent new];
return [CKTestLayoutComponent newWithChildren:{c1, c2}];
};
// Build first component generation:
auto const scopeRoot = CKComponentScopeRootWithPredicates(nil, nil, {}, {});
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Simulate state update on c1.childComponent
NSNumber *newState1 = @10;
CKComponentStateUpdateMap stateUpdates;
stateUpdates[c1.childComponent.treeNode.scopeHandle].push_back(^(id){ return newState1; });
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, NO);
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory, buildTrigger2, CKReflowTriggerNone);
// Verify `c2.childComponent` gets the correct new state
XCTAssertEqual(newState1, c1.childComponent.state);
// Verify c2 was reused
XCTAssertTrue(c2.didReuseComponent);
// Verify c1 wasn't reused
XCTAssertFalse(c1.didReuseComponent);
// Simulate state update on c2.childComponent
NSNumber *newState2 = @20;
CKComponentStateUpdateMap stateUpdates2;
stateUpdates2[c2.childComponent.treeNode.scopeHandle].push_back(^(id){ return newState2;});
auto const buildTrigger3 = CKBuildComponentTrigger(buildResults2.scopeRoot, stateUpdates2, NO, NO);
auto const buildResults3 = CKBuildComponent(buildResults2.scopeRoot, stateUpdates2, componentFactory, buildTrigger3, CKReflowTriggerNone);
// Verify `c2.childComponent` gets the correct new state
XCTAssertEqual(newState2, c2.childComponent.state);
// Verify c1 was reused
XCTAssertTrue(c1.didReuseComponent);
// Verify c2 wasn't reused
XCTAssertFalse(c2.didReuseComponent);
// Verify `c1.childComponent` preserves its state during component reuse
XCTAssertEqual(newState1, c1.childComponent.state);
// Simulate state update on c1.childComponent
NSNumber *newState3 = @30;
CKComponentStateUpdateMap stateUpdates3;
stateUpdates3[c1.childComponent.treeNode.scopeHandle].push_back(^(id){ return newState3;});
auto const buildTrigger4 = CKBuildComponentTrigger(buildResults3.scopeRoot, stateUpdates3, NO, NO);
auto const buildResults4 = CKBuildComponent(buildResults3.scopeRoot, stateUpdates3, componentFactory, buildTrigger4, CKReflowTriggerNone);
// Verify `c1.childComponent` gets the correct new state
XCTAssertEqual(newState3, c1.childComponent.state);
// Verify c2 was reused
XCTAssertTrue(c2.didReuseComponent);
// Verify c1 wasn't reused
XCTAssertFalse(c1.didReuseComponent);
// Verify `c2.childComponent` preserves its state during component reuse
XCTAssertEqual(newState2, c2.childComponent.state);
}
- (void)test_renderComponentPreserveStateDuringComponentReuse
{
// Build new tree with siblings `CKTestRenderWithNonRenderWithStateChildComponent` components.
// Each `CKTestRenderWithNonRenderWithStateChildComponent` has non-render component with state.
__block CKTestRenderWithNonRenderWithStateChildComponent *c1;
__block CKTestRenderWithNonRenderWithStateChildComponent *c2;
auto const componentFactory = ^{
c1 = [CKTestRenderWithNonRenderWithStateChildComponent new];
c2 = [CKTestRenderWithNonRenderWithStateChildComponent new];
return [CKTestLayoutComponent newWithChildren:{c1, c2}];
};
// Build first component generation:
auto const scopeRoot = CKComponentScopeRootWithPredicates(nil, nil, {}, {});
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Simulate state update on c1
NSNumber *newState1 = @10;
CKComponentStateUpdateMap stateUpdates;
stateUpdates[c1.treeNode.scopeHandle].push_back(^(id){ return newState1; });
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, NO);
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory, buildTrigger2, CKReflowTriggerNone);
// Verify `c2.childComponent` gets the correct new state
XCTAssertEqual(newState1, c1.state);
// Verify c2 was reused
XCTAssertTrue(c2.didReuseComponent);
// Verify c1 wasn't reused
XCTAssertFalse(c1.didReuseComponent);
// Simulate state update on c2
NSNumber *newState2 = @20;
CKComponentStateUpdateMap stateUpdates2;
stateUpdates2[c2.treeNode.scopeHandle].push_back(^(id){ return newState2;});
auto const buildTrigger3 = CKBuildComponentTrigger(buildResults2.scopeRoot, stateUpdates2, NO, NO);
auto const buildResults3 = CKBuildComponent(buildResults2.scopeRoot, stateUpdates2, componentFactory, buildTrigger3, CKReflowTriggerNone);
// Verify `c2.childComponent` gets the correct new state
XCTAssertEqual(newState2, c2.state);
// Verify c1 was reused
XCTAssertTrue(c1.didReuseComponent);
// Verify c2 wasn't reused
XCTAssertFalse(c2.didReuseComponent);
// Verify `c1.childComponent` preserves its state during component reuse
XCTAssertEqual(newState1, c1.state);
// Simulate state update on c1
NSNumber *newState3 = @30;
CKComponentStateUpdateMap stateUpdates3;
stateUpdates3[c1.treeNode.scopeHandle].push_back(^(id){ return newState3;});
auto const buildTrigger4 = CKBuildComponentTrigger(buildResults3.scopeRoot, stateUpdates3, NO, NO);
auto const buildResults4 = CKBuildComponent(buildResults3.scopeRoot, stateUpdates3, componentFactory, buildTrigger4, CKReflowTriggerNone);
// Verify `c1.childComponent` gets the correct new state
XCTAssertEqual(newState3, c1.state);
// Verify c2 was reused
XCTAssertTrue(c2.didReuseComponent);
// Verify c1 wasn't reused
XCTAssertFalse(c1.didReuseComponent);
// Verify `c2.childComponent` preserves its state during component reuse
XCTAssertEqual(newState2, c2.state);
}
- (void)test_nodeToParentLinksAfterReuse
{
// Build new tree with siblings `CKTestRenderWithNonRenderWithStateChildComponent` components.
// Each `CKTestRenderWithNonRenderWithStateChildComponent` has non-render component with state.
__block CKTestRenderWithNonRenderWithStateChildComponent *c1;
__block CKTestRenderWithNonRenderWithStateChildComponent *c2;
__block CKTestLayoutComponent *root;
auto const componentFactory = ^{
c1 = [CKTestRenderWithNonRenderWithStateChildComponent new];
c2 = [CKTestRenderWithNonRenderWithStateChildComponent new];
root = [CKTestLayoutComponent newWithChildren:{c1, c2}];
return root;
};
// Build first component generation:
auto const scopeRoot = CKComponentScopeRootWithPredicates(nil, nil, {}, {});
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Simulate state update on c1
NSNumber *newState1 = @10;
CKComponentStateUpdateMap stateUpdates;
stateUpdates[c1.treeNode.scopeHandle].push_back(^(id){ return newState1; });
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, NO);
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory, buildTrigger2, CKReflowTriggerNone);
// Verify `c2.childComponent` gets the correct new state
XCTAssertEqual(newState1, c1.state);
// Verify c2 was reused
XCTAssertTrue(c2.didReuseComponent);
// Verify c1 wasn't reused
XCTAssertFalse(c1.didReuseComponent);
NSMutableSet *components = [NSMutableSet setWithArray:@[c1, c2, c1.childComponent, c2.childComponent]];
NSMutableSet *componentsFromParentNodeMap = [NSMutableSet set];
// Add all the components from c1.childComponent to the root
id<CKComponentProtocol> component = c1.childComponent;
while (component) {
[componentsFromParentNodeMap addObject:component];
auto const parent = [buildResults2.scopeRoot rootNode].parentForNodeIdentifier(component.treeNode.nodeIdentifier);
component = parent.component;
}
// Add all the components from c2.childComponent to the root
id<CKComponentProtocol> component2 = c2.childComponent;
while (component2) {
[componentsFromParentNodeMap addObject:component2];
auto const parent = [buildResults2.scopeRoot rootNode].parentForNodeIdentifier(component2.treeNode.nodeIdentifier);
component2 = parent.component;
}
// Verify we got the same components from the node -> parent map as expected.
XCTAssertTrue([components isEqualToSet:componentsFromParentNodeMap]);
}
- (void)test_componentReuseWhenReorderScopedComponents
{
// Build new tree with siblings `CKCompositeComponentWithScope components
// tha thas `CKTestRenderWithNonRenderWithStateChildComponent` children.
// Each `CKTestRenderWithNonRenderWithStateChildComponent` has non-render component with state.
__block CKCompositeComponentWithScope *c1;
__block CKCompositeComponentWithScope *c2;
__block CKCompositeComponentWithScope *c3;
auto const componentFactory = ^{
c1 = [CKCompositeComponentWithScope newWithComponentProvider:^CKComponent *{
return [CKTestRenderWithNonRenderWithStateChildComponent new];
} scopeIdentifier:@1];
c2 = [CKCompositeComponentWithScope newWithComponentProvider:^CKComponent *{
return [CKTestRenderWithNonRenderWithStateChildComponent new];
} scopeIdentifier:@2];
return [CKTestLayoutComponent newWithChildren:{c1, c2}];
};
// Build first component generation:
auto const scopeRoot = CKComponentScopeRootWithPredicates(nil, nil, {}, {});
auto const buildTrigger = CKBuildComponentTrigger(scopeRoot, {}, NO, NO);
auto const buildResults = CKBuildComponent(scopeRoot, {}, componentFactory, buildTrigger, CKReflowTriggerNone);
// Insert new child c3 before c1
auto const componentFactory2 = ^{
c3 = [CKCompositeComponentWithScope newWithComponentProvider:^CKComponent *{
return [CKTestRenderWithNonRenderWithStateChildComponent new];
} scopeIdentifier:@3];
c1 = [CKCompositeComponentWithScope newWithComponentProvider:^CKComponent *{
return [CKTestRenderWithNonRenderWithStateChildComponent new];
} scopeIdentifier:@1];
c2 = [CKCompositeComponentWithScope newWithComponentProvider:^CKComponent *{
return [CKTestRenderWithNonRenderWithStateChildComponent new];
} scopeIdentifier:@2];
return [CKTestLayoutComponent newWithChildren:{c3, c1, c2}];
};
// Simulate state update on c1.child.childComponent
NSNumber *newState1 = @10;
CKComponentStateUpdateMap stateUpdates;
auto c1Child = (CKTestRenderWithNonRenderWithStateChildComponent *)c1.childComponent;
stateUpdates[c1Child.childComponent.treeNode.scopeHandle].push_back(^(id){ return newState1; });
auto const buildTrigger2 = CKBuildComponentTrigger(buildResults.scopeRoot, stateUpdates, NO, NO);
auto const buildResults2 = CKBuildComponent(buildResults.scopeRoot, stateUpdates, componentFactory2, buildTrigger2, CKReflowTriggerNone);
// Accessing the updated children
c1Child = (CKTestRenderWithNonRenderWithStateChildComponent *)c1.childComponent;
auto c2Child = (CKTestRenderWithNonRenderWithStateChildComponent *)c2.childComponent;
// Verify `c2.child.childComponent` gets the correct new state
XCTAssertEqual(newState1, c1Child.childComponent.state);
// Verify c2.child was reused
XCTAssertTrue(c2Child.didReuseComponent);
// Verify c1.child wasn't reused
XCTAssertFalse(c1Child.didReuseComponent);
}
@end