ComponentKitTests/CKComponentContextTests.mm (397 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/CKComponent.h> #import <ComponentKit/CKComponentContext.h> #import <ComponentKit/CKComponentScopeRootFactory.h> #import <ComponentKit/CKComponentCreationValidation.h> #import <ComponentKit/CKLayoutComponent.h> #import <ComponentKit/CKRenderComponent.h> #import <ComponentKitTestHelpers/CKComponentTestRootScope.h> @interface CKContextTestComponent<T> : CKComponent @property (nonatomic, strong) id<NSObject> objectFromContext; @end @interface CKContextTestRenderComponent<T> : CKRenderComponent + (instancetype)newWithContextObject:(NSNumber *)object; @property (nonatomic, strong) id<NSObject> objectFromContext; @property (nonatomic, strong) CKContextTestComponent *childTest; @end @interface CKContextTestWithChildrenComponent : CKLayoutComponent CK_INIT_UNAVAILABLE; CK_LAYOUT_COMPONENT_INIT_UNAVAILABLE; + (instancetype)newWithChildren:(std::vector<CKComponent *>)children; @property (nonatomic, assign) std::vector<CKComponent *>children; @end @interface CKComponentMutableContextTests : XCTestCase @end @implementation CKComponentMutableContextTests - (void)testEstablishingAComponentContextAllowsYouToFetchIt { NSObject *o = [[NSObject alloc] init]; CKComponentMutableContext<NSObject> context(o); NSObject *o2 = CKComponentMutableContext<NSObject>::get(); XCTAssertTrue(o == o2); } - (void)testFetchingAnObjectThatHasNotBeenEstablishedWithGetReturnsNil { XCTAssertNil(CKComponentMutableContext<NSObject>::get(), @"Expected to return nil without throwing"); } - (void)testComponentContextCleansUpWhenItGoesOutOfScope { { NSObject *o = [[NSObject alloc] init]; CKComponentMutableContext<NSObject> context(o); } XCTAssertNil(CKComponentMutableContext<NSObject>::get(), @"Expected getting NSObject to return nil as its scope is closed"); } - (void)testComponentContextDoesntCleansUpWhenItGoesOutOfScopeIfThereIsRenderComponentInSubtree { NSObject *o = [[NSObject alloc] init]; CKComponent *component = nil; { CKComponentTestRootScope testScope; component = [CKComponent new]; } { CKComponentMutableContext<NSObject> context(o); // This makes sure that the context values will leave after the context object goes out of scope. CKComponentContextHelper::didCreateRenderComponent(component); } CKComponentContextHelper::willBuildComponentTree(component); NSObject *o2 = CKComponentMutableContext<NSObject>::get(); XCTAssertTrue(o == o2); CKComponentContextHelper::didBuildComponentTree(component); XCTAssertNil(CKComponentMutableContext<NSObject>::get(), @"Expected getting NSObject to return nil as its scope is closed"); } - (void)testNestedComponentContextChangesValueAndRestoresItAfterGoingOutOfScope { NSObject *outer = [[NSObject alloc] init]; CKComponentMutableContext<NSObject> outerContext(outer); { NSObject *inner = [[NSObject alloc] init]; CKComponentMutableContext<NSObject> innerContext(inner); XCTAssertTrue(CKComponentMutableContext<NSObject>::get() == inner); } XCTAssertTrue(CKComponentMutableContext<NSObject>::get() == outer); } - (void)testSameContextInSiblingComponentsWithRenderInTheTree { NSNumber *n0 = @0; // +--------+ // |push(n0)| // | Root | // | | // | | // +----------+---+----+---------+ // | | | // | | | // +---v----+ +---v----+ +----v---+ // | | | | | | // | c1 | | c2 | | c3 | // |(render)| |(render)| |(render)| // | | | | | | // +---+----+ +---+----+ +----+---+ // | | | // | | | // +---v----+ +---v----+ +----v---+ // |read(n0)| |read(n0)| |read(n0)| // | child1 | | child2 | | child3 | // | | | | | | // | | | | | | // +--------+ +--------+ +--------+ __block CKContextTestRenderComponent *c1; __block CKContextTestRenderComponent *c2; __block CKContextTestRenderComponent *c3; auto const componentFactory = ^{ CKComponentMutableContext<NSNumber> context(n0); c1 = [CKContextTestRenderComponent new]; c2 = [CKContextTestRenderComponent new]; c3 = [CKContextTestRenderComponent new]; return [CKContextTestWithChildrenComponent newWithChildren:{c1,c2,c3}]; }; auto const buildResults = CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, componentFactory); XCTAssertTrue(n0 == c1.childTest.objectFromContext); XCTAssertTrue(n0 == c2.childTest.objectFromContext); XCTAssertTrue(n0 == c3.childTest.objectFromContext); } - (void)testSameContextInSiblingComponentsAndOverrideContextWithRenderInTheTree { NSNumber *n0 = @0; NSNumber *n1 = @1; NSNumber *n2 = @2; NSNumber *n3 = @3; // +--------+ // |push(n0)| // | Root | // | | // | | // +----------+---+----+---------+ // | | | // | | | // +---v----+ +---v----+ +----v---+ // |read(n0)| |read(n0)| |read(n0)| // | c1 | | c2 | | c3 | // |(render)| |(render)| |(render)| // |push(n1)| |push(n2)| |push(n3)| // +---+----+ +---+----+ +----+---+ // | | | // | | | // +---v----+ +---v----+ +----v---+ // |read(n1)| |read(n2)| |read(n3)| // | child1 | | child2 | | child3 | // | | | | | | // | | | | | | // +--------+ +--------+ +--------+ __block CKContextTestRenderComponent *c1; __block CKContextTestRenderComponent *c2; __block CKContextTestRenderComponent *c3; auto const componentFactory = ^{ CKComponentMutableContext<NSNumber> context(n0); c1 = [CKContextTestRenderComponent newWithContextObject:n1]; c2 = [CKContextTestRenderComponent newWithContextObject:n2]; c3 = [CKContextTestRenderComponent newWithContextObject:n3]; return [CKContextTestWithChildrenComponent newWithChildren:{c1,c2,c3}]; }; auto const buildResults = CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, componentFactory); XCTAssertTrue(n0 == c1.objectFromContext); XCTAssertTrue(n0 == c2.objectFromContext); XCTAssertTrue(n0 == c3.objectFromContext); XCTAssertTrue(n1 == c1.childTest.objectFromContext); XCTAssertTrue(n2 == c2.childTest.objectFromContext); XCTAssertTrue(n3 == c3.childTest.objectFromContext); } - (void)testTriplyNestedComponentContextWithNilMiddleValueCorrectlyRestoresOuterValue { // This tests an obscure edge case with restoring values for context as we pop scopes. NSObject *outer = [[NSObject alloc] init]; CKComponentMutableContext<NSObject> outerContext(outer); { CKComponentMutableContext<NSObject> middleContext(nil); XCTAssertTrue(CKComponentMutableContext<NSObject>::get() == nil); { NSObject *inner = [[NSObject alloc] init]; CKComponentMutableContext<NSObject> innerContext(inner); XCTAssertTrue(CKComponentMutableContext<NSObject>::get() == inner); } } XCTAssertTrue(CKComponentMutableContext<NSObject>::get() == outer); } - (void)testFetchingAllComponentContextItemsReturnsObjects { NSObject *o = [[NSObject alloc] init]; CKComponentMutableContext<NSObject> context(o); const CKComponentContextContents contents = CKComponentContextHelper::fetchAll(); const auto expectedValues = [[NSDictionary alloc] initWithObjects:@[o] forKeys:@[[NSObject class]]]; XCTAssertEqualObjects(contents.objects, expectedValues); } - (void)testFetchingAllComponentContextItemsTwiceReturnsEqualContents { CKComponentMutableContext<NSObject> context([[NSObject alloc] init]); const CKComponentContextContents contents1 = CKComponentContextHelper::fetchAll(); const CKComponentContextContents contents2 = CKComponentContextHelper::fetchAll(); XCTAssertTrue(contents1 == contents2); } - (void)testFetchingAllComponentContextItemsBeforeAndAfterModificationReturnsUnequalContents { CKComponentMutableContext<NSObject> context1([[NSObject alloc] init]); const CKComponentContextContents contents1 = CKComponentContextHelper::fetchAll(); CKComponentMutableContext<NSObject> context2([[NSObject alloc] init]); const CKComponentContextContents contents2 = CKComponentContextHelper::fetchAll(); XCTAssertTrue(contents1 != contents2); } - (void)testFetchingAllComponentContextWhenRenderComponentIsInTheTree { NSObject *o1 = [NSObject alloc]; NSObject *o2 = [NSObject alloc]; CKComponent *component1; { CKComponentMutableContext<NSObject> context1(o1); const auto expectedValues = [[NSDictionary alloc] initWithObjects:@[o1] forKeys:@[[NSObject class]]]; XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, expectedValues); { CKComponentTestRootScope testScope; // Simulate creation of render component. component1 = [CKComponent new]; } CKComponentContextHelper::didCreateRenderComponent(component1); } CKComponentContextHelper::willBuildComponentTree(component1); // As there is a render component in the tree, o1 still need to stay in the store. const auto expectedValues = [[NSDictionary alloc] initWithObjects:@[o1] forKeys:@[[NSObject class]]]; XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, expectedValues); { CKComponentMutableContext<NSObject> context1(o2); const auto expectedValues2 = [[NSDictionary alloc] initWithObjects:@[o2] forKeys:@[[NSObject class]]]; // Make sure we get the latest value from the current store. XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, expectedValues2); } XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, expectedValues); CKComponentContextHelper::didBuildComponentTree(component1); XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, nil); } #pragma mark - Initial Values - (void)testInitialValues { // Verify the value is nil at first. XCTAssertNil(CKComponentMutableContext<NSObject>::get()); // Set initial values and make sure the value is available. NSObject *o = [[NSObject alloc] init]; const auto initialValues = [[NSDictionary alloc] initWithObjects:@[o] forKeys:@[[NSObject class]]]; { CKComponentInitialValuesContext initialValuesContext(initialValues); XCTAssertEqualObjects(CKComponentMutableContext<NSObject>::get(), o); } // Verify the values have been cleaned. XCTAssertNil(CKComponentMutableContext<NSObject>::get()); } - (void)testInitialValuesWithOvrride { // Set initial values and make sure the value is available. NSObject *o = [[NSObject alloc] init]; const auto initialValues = [[NSDictionary alloc] initWithObjects:@[o] forKeys:@[[NSObject class]]]; { CKComponentInitialValuesContext initialValuesContext(initialValues); XCTAssertEqualObjects(CKComponentMutableContext<NSObject>::get(), o); // Push context with the same key { NSObject *o2 = [[NSObject alloc] init]; CKComponentMutableContext<NSObject> context(o2); XCTAssertEqualObjects(CKComponentMutableContext<NSObject>::get(), o2); } // Check that the initial value is being fetched correctly. XCTAssertEqualObjects(CKComponentMutableContext<NSObject>::get(), o); } // Verify the values have been cleaned. XCTAssertNil(CKComponentContextHelper::fetchAll().objects); } - (void)testFetchAllForInitialValues { NSObject *o = [[NSObject alloc] init]; const auto initialValues = [[NSDictionary alloc] initWithObjects:@[o] forKeys:@[[NSObject class]]]; { // Set initial values CKComponentInitialValuesContext initialValuesContext(initialValues); { // Push context NSString *s = @"1"; CKComponentMutableContext<NSString> context(s); const auto expectedValue = [[NSDictionary alloc] initWithObjects:@[o, s] forKeys:@[[NSObject class], [NSString class]]]; XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, expectedValue); } // Verify the initial values are being fetched correctly. XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, initialValues); } // Verify the values have been cleaned. XCTAssertNil(CKComponentContextHelper::fetchAll().objects); } @end @interface CKComponentContextTests : XCTestCase @end @implementation CKComponentContextTests - (void)testEstablishingAComponentContextAllowsYouToFetchIt { NSObject *o = [[NSObject alloc] init]; CKComponentContext<NSObject> context(o); NSObject *o2 = CKComponentContext<NSObject>::get(); XCTAssertTrue(o == o2); } - (void)testFetchingAnObjectThatHasNotBeenEstablishedWithGetReturnsNil { XCTAssertNil(CKComponentContext<NSObject>::get(), @"Expected to return nil without throwing"); } - (void)testComponentContextCleansUpWhenItGoesOutOfScope { { NSObject *o = [[NSObject alloc] init]; CKComponentContext<NSObject> context(o); } XCTAssertNil(CKComponentContext<NSObject>::get(), @"Expected getting NSObject to return nil as its scope is closed"); } - (void)testComponentContextDoesntCleansUpWhenItGoesOutOfScopeIfThereIsRenderComponentInSubtree { NSObject *o = [[NSObject alloc] init]; CKComponent *component = nil; { CKComponentTestRootScope testScope; component = [CKComponent new]; } { CKComponentContext<NSObject> context(o); // This makes sure that the context values will leave after the context object goes out of scope. CKComponentContextHelper::didCreateRenderComponent(component); } CKComponentContextHelper::willBuildComponentTree(component); NSObject *o2 = CKComponentContext<NSObject>::get(); XCTAssertTrue(o == o2); CKComponentContextHelper::didBuildComponentTree(component); XCTAssertNil(CKComponentContext<NSObject>::get(), @"Expected getting NSObject to return nil as its scope is closed"); } - (void)testNestedComponentContextChangesValueAndRestoresItAfterGoingOutOfScope { NSObject *outer = [[NSObject alloc] init]; CKComponentContext<NSObject> outerContext(outer); { NSObject *inner = [[NSObject alloc] init]; CKComponentContext<NSObject> innerContext(inner); XCTAssertTrue(CKComponentContext<NSObject>::get() == inner); } XCTAssertTrue(CKComponentContext<NSObject>::get() == outer); } - (void)testTriplyNestedComponentContextWithNilMiddleValueCorrectlyRestoresOuterValue { // This tests an obscure edge case with restoring values for context as we pop scopes. NSObject *outer = [[NSObject alloc] init]; CKComponentContext<NSObject> outerContext(outer); { CKComponentContext<NSObject> middleContext(nil); XCTAssertTrue(CKComponentContext<NSObject>::get() == nil); { NSObject *inner = [[NSObject alloc] init]; CKComponentContext<NSObject> innerContext(inner); XCTAssertTrue(CKComponentContext<NSObject>::get() == inner); } } XCTAssertTrue(CKComponentContext<NSObject>::get() == outer); } - (void)testFetchingAllComponentContextItemsReturnsObjects { NSObject *o = [[NSObject alloc] init]; CKComponentContext<NSObject> context(o); const CKComponentContextContents contents = CKComponentContextHelper::fetchAll(); const auto expectedValues = [[NSDictionary alloc] initWithObjects:@[o] forKeys:@[[NSObject class]]]; XCTAssertEqualObjects(contents.objects, expectedValues); } - (void)testFetchingAllComponentContextItemsTwiceReturnsEqualContents { CKComponentContext<NSObject> context([[NSObject alloc] init]); const CKComponentContextContents contents1 = CKComponentContextHelper::fetchAll(); const CKComponentContextContents contents2 = CKComponentContextHelper::fetchAll(); XCTAssertTrue(contents1 == contents2); } - (void)testFetchingAllComponentContextItemsBeforeAndAfterModificationReturnsUnequalContents { CKComponentContext<NSObject> context1([[NSObject alloc] init]); const CKComponentContextContents contents1 = CKComponentContextHelper::fetchAll(); CKComponentContext<NSObject> context2([[NSObject alloc] init]); const CKComponentContextContents contents2 = CKComponentContextHelper::fetchAll(); XCTAssertTrue(contents1 != contents2); } - (void)testFetchingAllComponentContextWhenRenderComponentIsInTheTree { NSObject *o1 = [NSObject alloc]; NSObject *o2 = [NSObject alloc]; CKComponent *component1 = nil; { CKComponentTestRootScope testScope; component1 = [CKComponent new]; } const auto expectedValues = [[NSDictionary alloc] initWithObjects:@[o1] forKeys:@[[NSObject class]]]; { CKComponentContext<NSObject> context1(o1); XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, expectedValues); // Simulate creation of render component. { CKComponentTestRootScope testScope; component1 = [CKComponent new]; } CKComponentContextHelper::didCreateRenderComponent(component1); } CKComponentContextHelper::willBuildComponentTree(component1); // As there is a render component in the tree, o1 still need to stay in the store. XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, expectedValues); { CKComponentContext<NSObject> context1(o2); // Make sure we get the latest value from the current store. const auto expectedValues2 = [[NSDictionary alloc] initWithObjects:@[o2] forKeys:@[[NSObject class]]]; XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, expectedValues2); } XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, expectedValues); CKComponentContextHelper::didBuildComponentTree(component1); XCTAssertEqualObjects(CKComponentContextHelper::fetchAll().objects, nil); } @end #pragma mark - Helpers @implementation CKContextTestComponent + (instancetype)new { id objectFromContext = CKComponentMutableContext<NSNumber>::get(); auto const c = [super new]; if (c) { c->_objectFromContext = objectFromContext; } return c; } @end @implementation CKContextTestRenderComponent + (instancetype)newWithContextObject:(NSNumber *)object { // Read the existing value from context. NSNumber *objectFromContext = CKComponentMutableContext<NSNumber>::get(); // Override push new context with the same key CKComponentMutableContext<NSNumber> context(object); auto const c = [super new]; if (c) { c->_objectFromContext = objectFromContext; } return c; } - (CKComponent *)render:(id)state { _childTest = [CKContextTestComponent new]; return _childTest; } @end @implementation CKContextTestWithChildrenComponent + (instancetype)newWithChildren:(std::vector<CKComponent *>)children { auto const c = [super new]; if (c) { c->_children = children; } return c; } - (unsigned int)numberOfChildren { return (unsigned int)_children.size(); } - (id<CKMountable>)childAtIndex:(unsigned int)index { if (index < _children.size()) { return _children[index]; } RCFailAssertWithCategory([self class], @"Index %u is out of bounds %lu", index, _children.size()); return nil; } @end