ComponentKitTests/CKBuildComponentTreeTests.mm (203 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/CKFlexboxComponent.h> #import <ComponentKitTestHelpers/CKRenderComponentTestHelpers.h> #import <ComponentKit/CKMountable.h> #import <ComponentKit/CKRenderHelpers.h> #import <ComponentKit/CKComponent.h> #import <ComponentKit/CKCompositeComponent.h> #import <ComponentKit/CKRenderComponent.h> #import <ComponentKit/CKComponentInternal.h> #import <ComponentKit/CKButtonComponent.h> #import <ComponentKit/CKComponentScopeRootFactory.h> #import <ComponentKit/CKThreadLocalComponentScope.h> #import <ComponentKit/CKTreeNode.h> #import "CKComponentTestCase.h" /** A helper class that inherits from 'CKRenderComponent'; render the component from the initializer */ @interface CKComponentTreeTestComponent_Render : CKRenderComponent + (instancetype)newWithComponent:(CKComponent *)component; @end /** A helper class that inherits from 'CKRenderComponent' and render a random CKComponent */ @interface CKComponentTreeTestComponent_RenderWithChild : CKRenderComponent @property (nonatomic, strong) CKCompositeComponentWithScopeAndState *childComponent; @property (nonatomic, assign) BOOL hasReusedComponent; @end #pragma mark - Tests @interface CKBuildComponentTreeTests : CKComponentTestCase @end @implementation CKBuildComponentTreeTests #pragma mark - CKComponent - (void)test_buildComponentTree_onCKComponent { auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil); auto const root = [scopeRoot rootNode].node(); CKComponent *c = [CKComponentTreeTestComponent_Render new]; [c buildComponentTree:root previousParent:nil params:{ .scopeRoot = scopeRoot, .stateUpdates = {}, .buildTrigger = CKBuildTriggerNone, .treeNodeDirtyIds = {}, } parentHasStateUpdate:NO]; XCTAssertEqual(root.children.size(), 1); XCTAssertEqual(root.children[0].component, c); // Simulate a second tree creation. auto const scopeRoot2 = [scopeRoot.asNullable() newRoot]; auto const root2 = [scopeRoot2 rootNode].node(); CKComponent *c2 = [CKComponentTreeTestComponent_Render new]; [c2 buildComponentTree:root2 previousParent:root params:{ .scopeRoot = scopeRoot2, .previousScopeRoot = scopeRoot, .stateUpdates = {}, .buildTrigger = CKBuildTriggerPropsUpdate, .treeNodeDirtyIds = {}, } parentHasStateUpdate:NO]; XCTAssertTrue(areTreesEqual(root, root2)); } #pragma mark - CKRenderComponent - (void)test_buildComponentTree_onCKRenderComponent { auto const scopeRoot = CKComponentScopeRootWithDefaultPredicates(nil, nil); auto const root = [scopeRoot rootNode].node(); CKComponent *c = [CKComponentTreeTestComponent_Render new]; CKRenderComponent *renderComponent = [CKComponentTreeTestComponent_Render newWithComponent:c]; [renderComponent buildComponentTree:root previousParent:nil params:{ .scopeRoot = scopeRoot, .stateUpdates = {}, .buildTrigger = CKBuildTriggerNone, .treeNodeDirtyIds = {}, } parentHasStateUpdate:NO]; // Make sure the root has only one child. XCTAssertEqual(root.children.size(), 1); CKTreeNode *singleChildNode = root.children[0]; verifyChildToParentConnection(root, singleChildNode, renderComponent); // Check the next level of the tree XCTAssertEqual(singleChildNode.children.size(), 1); CKTreeNode *componentNode = singleChildNode.children[0]; verifyChildToParentConnection(singleChildNode, componentNode, c); // Simulate a second tree creation. auto const scopeRoot2 = [scopeRoot.asNullable() newRoot]; auto const root2 = [scopeRoot2 rootNode].node(); CKComponent *c2 = [CKComponentTreeTestComponent_Render new]; CKRenderComponent *renderComponent2 = [CKComponentTreeTestComponent_Render newWithComponent:c2]; [renderComponent2 buildComponentTree:root2 previousParent:root params:{ .scopeRoot = scopeRoot2, .previousScopeRoot = scopeRoot, .stateUpdates = {}, .buildTrigger = CKBuildTriggerPropsUpdate, .treeNodeDirtyIds = {}, } parentHasStateUpdate:NO]; XCTAssertTrue(areTreesEqual(root, root2)); } #pragma mark - CKLayoutComponent - (void)test_buildComponentTree_onCKLayoutComponent { CKThreadLocalComponentScope threadScope(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}); auto const scopeRoot = threadScope.newScopeRoot; auto const root = [CKTreeNode rootNode]; CKComponent *c10 = [CKComponentTreeTestComponent_Render new]; CKComponent *c11 = [CKComponentTreeTestComponent_Render new]; auto renderWithChidlrenComponent = [CKTestLayoutComponent newWithChildren:{c10, c11}]; [renderWithChidlrenComponent buildComponentTree:root previousParent:nil params:{ .scopeRoot = scopeRoot, .previousScopeRoot = nil, .stateUpdates = {}, .buildTrigger = CKBuildTriggerNone, .treeNodeDirtyIds = {}, } parentHasStateUpdate:NO]; XCTAssertEqual(root.children.size(), 2); XCTAssertTrue(verifyComponentsInNode(root, @[c10, c11])); // Simulate a second tree creation. auto const scopeRoot2 = [scopeRoot.asNullable() newRoot]; auto const root2 = scopeRoot2.rootNode.node(); CKComponent *c20 = [CKComponentTreeTestComponent_Render new]; CKComponent *c21 = [CKComponentTreeTestComponent_Render new]; auto renderWithChidlrenComponent2 = [CKTestLayoutComponent newWithChildren:{c20, c21}]; [renderWithChidlrenComponent2 buildComponentTree:root2 previousParent:root params:{ .scopeRoot = scopeRoot2, .previousScopeRoot = scopeRoot, .stateUpdates = {}, .buildTrigger = CKBuildTriggerPropsUpdate, .treeNodeDirtyIds = {}, } parentHasStateUpdate:NO]; XCTAssertTrue(areTreesEqual(root, root2)); } #pragma mark - Build Component Helpers - (void)test_renderComponentHelpers_treeNodeDirtyIdsFor_onNewTreeAndPropsUpdate { XCTAssertTrue(CKRender::treeNodeDirtyIdsFor(nil, {}, CKBuildTriggerNone).empty(), @"It is not expected to have dirty nodes when new tree generation is triggered"); XCTAssertTrue(CKRender::treeNodeDirtyIdsFor(nil, {}, CKBuildTriggerPropsUpdate).empty(), @"It is not expected to have dirty nodes on tree generation triggered by props update"); XCTAssertTrue(CKRender::treeNodeDirtyIdsFor(nil, {}, CKBuildTriggerNone).empty(), @"It is not expected to have dirty nodes when new tree generation is triggered"); XCTAssertTrue(CKRender::treeNodeDirtyIdsFor(nil, {}, CKBuildTriggerPropsUpdate).empty(), @"It is not expected to have dirty nodes on tree generation triggered by props update"); } - (void)test_renderComponentHelpers_treeNodeDirtyIdsFor_onStateUpdate { __block CKComponentTreeTestComponent_Render *c; __block CKCompositeComponentWithScopeAndState *rootComponent; auto const componentFactory = ^{ c = [CKComponentTreeTestComponent_Render newWithComponent:CK::ComponentBuilder().build()]; rootComponent = [CKCompositeComponentWithScopeAndState newWithComponent:c]; return rootComponent; }; auto const buildResults = CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, componentFactory); // Simulate a state update on the root. CKComponentStateUpdateMap stateUpdates; stateUpdates[rootComponent.treeNode.scopeHandle].push_back(^(id){ return @2; }); CKTreeNodeDirtyIds dirtyNodeIds = CKRender::treeNodeDirtyIdsFor(buildResults.scopeRoot, stateUpdates, CKBuildTriggerStateUpdate); CKTreeNodeDirtyIds expectedDirtyNodeIds = {rootComponent.treeNode.scopeHandle.treeNodeIdentifier}; XCTAssertEqual(dirtyNodeIds, expectedDirtyNodeIds); } #pragma mark - Helpers static BOOL verifyChildToParentConnection(CKTreeNode *parentNode, CKTreeNode *childNode, CKComponent *c) { auto const componentKey = [childNode componentKey]; auto const childComponent = [parentNode childForComponentKey:componentKey].component; return [childComponent isEqual:c]; } /** Compare the components array to the components in the children nodes of 'parentNode' */ static BOOL verifyComponentsInNode(CKTreeNode *parentNode, NSArray<CKComponent *> *components) { // Verify that the root holds two components has its direct children NSMutableSet<CKComponent *> *componentsFromTheTree = [NSMutableSet set]; for (auto node : parentNode.children) { [componentsFromTheTree addObject:(CKComponent *)node.component]; } NSSet<id<CKMountable>> *componentsSet = [NSSet setWithArray:components]; return [componentsSet isEqualToSet:componentsFromTheTree]; } /** 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]; } /** 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); } } @end #pragma mark - Helper classes @implementation CKComponentTreeTestComponent_Render { CKComponent *_component; } + (instancetype)newWithComponent:(CKComponent *)component { auto const c = [super new]; if (c) { c->_component = component; } return c; } - (CKComponent *)render:(id)state { return _component; } @end @implementation CKComponentTreeTestComponent_RenderWithChild - (BOOL)requiresScopeHandle { return YES; } - (CKComponent *)render:(id)state { _childComponent = [CKCompositeComponentWithScopeAndState newWithComponent:[CKComponent new]]; return _childComponent; } - (void)didReuseComponent:(CKComponentTreeTestComponent_RenderWithChild *)component { _childComponent = component->_childComponent; _hasReusedComponent = YES; } @end