ComponentKitTests/TransactionalDataSource/CKDataSourceIntegrationTests.mm (276 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 <OCMock/OCMock.h> #import <Foundation/Foundation.h> #import <ComponentKitTestHelpers/CKTestRunLoopRunning.h> #import <ComponentKit/CKComponent.h> #import <ComponentKit/CKComponentProvider.h> #import <ComponentKit/CKComponentScope.h> #import <ComponentKit/CKComponentSubclass.h> #import <ComponentKit/CKCompositeComponent.h> #import <ComponentKit/CKComponentController.h> #import <ComponentKit/CKCollectionViewDataSource.h> #import <ComponentKit/CKDataSourceConfiguration.h> #import <ComponentKit/CKDataSourceConfigurationInternal.h> #import <ComponentKit/CKDataSourceChangeset.h> static NSString *const kOverrideDidPrepareLayoutForComponent = @"kOverrideDidPrepareLayoutForComponent"; @interface CKDataSourceIntegrationTestComponent : CKCompositeComponent @end @interface CKDataSourceIntegrationTestComponentController : CKComponentController @property (strong) NSMutableArray *callbacks; @end @implementation CKDataSourceIntegrationTestComponent + (instancetype)newWithIdentifier:(id)identifier { CKComponentScope scope(self, identifier); return [self newWithComponent:[CKComponent new]]; } + (Class<CKComponentControllerProtocol>)controllerClass { return [CKDataSourceIntegrationTestComponentController class]; } @end @implementation CKDataSourceIntegrationTestComponentController - (instancetype)initWithComponent:(CKComponent *)component { if ((self = [super initWithComponent:component])) { _callbacks = [NSMutableArray array]; } return self; } - (void)willUpdateComponent { [super willUpdateComponent]; [self.callbacks addObject:NSStringFromSelector(_cmd)]; } - (void)willRemount { [super willRemount]; [self.callbacks addObject:NSStringFromSelector(_cmd)]; } - (void)didRemount { [super didRemount]; [self.callbacks addObject:NSStringFromSelector(_cmd)]; } - (void)didUpdateComponent { [super didUpdateComponent]; [self.callbacks addObject:NSStringFromSelector(_cmd)]; } - (void)invalidateController { [super invalidateController]; [self.callbacks addObject:NSStringFromSelector(_cmd)]; } @end /** We will use this component in order to override the 'didPrepareLayoutForComponent'. */ @interface CKDataSourceIntegrationOverrideDidPrepareLayoutForComponentTestComponentController : CKDataSourceIntegrationTestComponentController @property (nonatomic, strong) NSMutableArray<NSString *> *layoutComponentsFromCallbacks; @property (nonatomic, strong) NSMutableArray<NSString *> *componentsFromCallbacks; @end @interface CKDataSourceIntegrationOverrideDidPrepareLayoutForComponentTestComponent : CKDataSourceIntegrationTestComponent @end @implementation CKDataSourceIntegrationOverrideDidPrepareLayoutForComponentTestComponent + (Class<CKComponentControllerProtocol>)controllerClass { return [CKDataSourceIntegrationOverrideDidPrepareLayoutForComponentTestComponentController class]; } @end @implementation CKDataSourceIntegrationOverrideDidPrepareLayoutForComponentTestComponentController - (instancetype)initWithComponent:(CKComponent *)component { if (self = [super initWithComponent:component]) { _layoutComponentsFromCallbacks = [NSMutableArray array]; _componentsFromCallbacks = [NSMutableArray array]; } return self; } - (void)didPrepareLayout:(const RCLayout &)layout forComponent:(CKComponent *)component { [self.callbacks addObject:NSStringFromSelector(_cmd)]; [self.layoutComponentsFromCallbacks addObject:[NSString stringWithFormat:@"%p",layout.component]]; [self.componentsFromCallbacks addObject:[NSString stringWithFormat:@"%p",component]]; } @end /** Tests start here. */ static NSMutableArray<CKComponent *> *g_components; static NSMutableDictionary<NSString *, CKComponent *> *g_componentsDictionary; @interface CKDataSourceIntegrationTests : XCTestCase @property (strong) CKCollectionViewDataSource *dataSource; @property (strong) CKDataSourceIntegrationTestComponentController *componentController; @property (assign) CGSize itemSize; @end @implementation CKDataSourceIntegrationTests - (void)setUp { [super setUp]; self.itemSize = CGSizeMake(320, 480); g_components = [NSMutableArray new]; g_componentsDictionary = [NSMutableDictionary dictionary]; self.dataSource = [self generateDataSource]; [self.dataSource applyChangeset: [[[[CKDataSourceChangesetBuilder new] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] withInsertedItems:@{ [NSIndexPath indexPathForItem:0 inSection:0] : @"" }] build] mode:CKUpdateModeSynchronous userInfo:nil]; XCTAssertEqual(g_components.count, 1); XCTAssertNotNil(g_components.lastObject.controller); XCTAssertTrue([g_components.lastObject.controller isKindOfClass:[CKDataSourceIntegrationTestComponentController class]]); self.componentController = (CKDataSourceIntegrationTestComponentController*) g_components.lastObject.controller; } - (CKCollectionViewDataSource *)generateDataSource { UICollectionViewFlowLayout *flowLayout = [UICollectionViewFlowLayout new]; flowLayout.itemSize = self.itemSize; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, self.itemSize.width, self.itemSize.height) collectionViewLayout:flowLayout]; CKDataSourceConfiguration *config = [[CKDataSourceConfiguration alloc] initWithComponentProviderFunc:componentProvider context:nil sizeRange:CKSizeRange(self.itemSize, self.itemSize) options:{} componentPredicates:{} componentControllerPredicates:{} analyticsListener:nil]; return [[CKCollectionViewDataSource alloc] initWithCollectionView:collectionView supplementaryViewDataSource:nil configuration:config]; } static CKComponent *componentProvider(id<NSObject> untypedModel, id<NSObject> _) { NSString *const model = (NSString *)untypedModel; Class klass = [model isEqualToString:kOverrideDidPrepareLayoutForComponent] ? [CKDataSourceIntegrationOverrideDidPrepareLayoutForComponentTestComponent class] : [CKDataSourceIntegrationTestComponent class]; CKComponent *component = [klass newWithIdentifier:model]; [g_components addObject:component]; g_componentsDictionary[model] = component; return component; } - (void)testUpdateModelShouldCreateNewComponentAndTriggerControllerCallbacksForRemount { [self.dataSource applyChangeset: [[[CKDataSourceChangesetBuilder new] withUpdatedItems:@{[NSIndexPath indexPathForItem:0 inSection:0] : @""}] build] mode:CKUpdateModeSynchronous userInfo:nil]; XCTAssertEqual(g_components.count, 2); XCTAssertEqualObjects(self.componentController.callbacks, (@[ NSStringFromSelector(@selector(willUpdateComponent)), NSStringFromSelector(@selector(willRemount)), NSStringFromSelector(@selector(didRemount)), NSStringFromSelector(@selector(didUpdateComponent)) ])); } - (void)testUpdateModelAlwaysSendUpdateControllerCallbacks_Off { self.dataSource = [self generateDataSource]; [self.dataSource applyChangeset: [[[[CKDataSourceChangesetBuilder new] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] withInsertedItems:@{ [NSIndexPath indexPathForItem:0 inSection:0] : @"0", [NSIndexPath indexPathForItem:1 inSection:0] : @"1", [NSIndexPath indexPathForItem:2 inSection:0] : @"2", }] build] mode:CKUpdateModeSynchronous userInfo:nil]; [self.dataSource applyChangeset: [[[CKDataSourceChangesetBuilder new] withUpdatedItems:@{[NSIndexPath indexPathForItem:2 inSection:0] : @"2"}] build] mode:CKUpdateModeSynchronous userInfo:nil]; CKDataSourceIntegrationTestComponentController *controller = (CKDataSourceIntegrationTestComponentController*)g_componentsDictionary[@"2"].controller; // We use 'CKTestConfigDefault' and item is out of the view port. It means it shoudn't get any update. XCTAssertEqualObjects(controller.callbacks, (@[NSStringFromSelector(@selector(willUpdateComponent))])); } - (void)testUpdateModelAlwaysSendUpdateControllerCallbacks_didPrepareLayoutForComponent_off { self.dataSource = [self generateDataSource]; [self.dataSource applyChangeset: [[[[CKDataSourceChangesetBuilder new] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] withInsertedItems:@{ [NSIndexPath indexPathForItem:0 inSection:0] : @"0", [NSIndexPath indexPathForItem:1 inSection:0] : @"1", [NSIndexPath indexPathForItem:2 inSection:0] : @"2", }] build] mode:CKUpdateModeSynchronous userInfo:nil]; CKDataSourceIntegrationTestComponentController *controller = (CKDataSourceIntegrationTestComponentController*)g_componentsDictionary[@"2"].controller; XCTAssertEqualObjects(controller.callbacks, (@[])); [self.dataSource applyChangeset: [[[CKDataSourceChangesetBuilder new] withUpdatedItems:@{[NSIndexPath indexPathForItem:2 inSection:0] : @"2"}] build] mode:CKUpdateModeSynchronous userInfo:nil]; controller = (CKDataSourceIntegrationTestComponentController*)g_componentsDictionary[@"2"].controller; XCTAssertEqualObjects(controller.callbacks, (@[NSStringFromSelector(@selector(willUpdateComponent))])); [self.dataSource applyChangeset: [[[CKDataSourceChangesetBuilder new] withMovedItems:(@{[NSIndexPath indexPathForItem:1 inSection:0] : [NSIndexPath indexPathForItem:2 inSection:0]})] build] mode:CKUpdateModeSynchronous userInfo:nil]; controller = (CKDataSourceIntegrationTestComponentController*)g_components.lastObject.controller; XCTAssertEqualObjects(controller.callbacks, (@[NSStringFromSelector(@selector(willUpdateComponent))])); } - (void)testUpdateModelAlwaysSendUpdateControllerCallbacks_didPrepareLayoutForComponent_on { self.dataSource = [self generateDataSource]; // Test 'didPrepareLayoutForComponent:' during insert. [self.dataSource applyChangeset: [[[[CKDataSourceChangesetBuilder new] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] withInsertedItems:@{ [NSIndexPath indexPathForItem:0 inSection:0] : @"0", [NSIndexPath indexPathForItem:1 inSection:0] : @"1", [NSIndexPath indexPathForItem:2 inSection:0] : kOverrideDidPrepareLayoutForComponent, }] build] mode:CKUpdateModeSynchronous userInfo:nil]; CKDataSourceIntegrationOverrideDidPrepareLayoutForComponentTestComponentController *controller = (CKDataSourceIntegrationOverrideDidPrepareLayoutForComponentTestComponentController*)g_componentsDictionary[kOverrideDidPrepareLayoutForComponent].controller; XCTAssertEqualObjects(controller.callbacks, (@[ NSStringFromSelector(@selector(didPrepareLayout:forComponent:)) ])); // Test 'didPrepareLayoutForComponent:' during update. [self.dataSource applyChangeset: [[[[CKDataSourceChangesetBuilder new] withInsertedItems: @{ [NSIndexPath indexPathForItem:0 inSection:0] : @"0.1"}] withUpdatedItems: @{ [NSIndexPath indexPathForItem:2 inSection:0] : kOverrideDidPrepareLayoutForComponent }] build] mode:CKUpdateModeSynchronous userInfo:nil]; controller = (CKDataSourceIntegrationOverrideDidPrepareLayoutForComponentTestComponentController*)g_componentsDictionary[kOverrideDidPrepareLayoutForComponent].controller; XCTAssertEqualObjects(controller.callbacks, (@[ NSStringFromSelector(@selector(didPrepareLayout:forComponent:)), NSStringFromSelector(@selector(willUpdateComponent)), NSStringFromSelector(@selector(didPrepareLayout:forComponent:)) ])); // Make sure that we can the correct layout in the "didPrepareLayout:forComponent:" by comparing the address of the component and layout.component. for (NSUInteger i=0; i<controller.layoutComponentsFromCallbacks.count; i++) { NSString *componentLayoutAddress = controller.layoutComponentsFromCallbacks[i]; NSString *componentAddress = controller.componentsFromCallbacks[i]; XCTAssertEqualObjects(componentLayoutAddress, componentAddress); } } // This test checks that controller receives invalidateController callback when DataSource owning it // applies change that removes it from the state - (void)testComponentControllerReceivesInvalidateEventWhenRemoved { [self.dataSource applyChangeset: [[[CKDataSourceChangesetBuilder new] withRemovedItems:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]] build] mode:CKUpdateModeSynchronous userInfo:nil]; self.dataSource = nil; XCTAssertEqualObjects(self.componentController.callbacks, (@[ NSStringFromSelector(@selector(invalidateController)), ])); } // This test checks that controller receives invalidateController callback when DataSource owning it is destroyed - (void)testComponentControllerReceivesInvalidateEventDuringDeallocation { NSArray *callbacks = nil; @autoreleasepool { self.dataSource = [self generateDataSource]; [self.dataSource applyChangeset: [[[[CKDataSourceChangesetBuilder new] withInsertedSections:[NSIndexSet indexSetWithIndex:0]] withInsertedItems:@{ [NSIndexPath indexPathForItem:0 inSection:0] : @"" }] build] mode:CKUpdateModeSynchronous userInfo:nil]; CKDataSourceIntegrationTestComponentController * controller = (CKDataSourceIntegrationTestComponentController*) g_components.lastObject.controller; callbacks = controller.callbacks; // We clean everything to ensure dataSource receives deallocation happens when autorelease pool is destroyed self.dataSource = nil; } CKRunRunLoopUntilBlockIsTrue(^BOOL{ return callbacks.count > 0; }); XCTAssertEqualObjects(callbacks, (@[NSStringFromSelector(@selector(invalidateController))])); } @end