ComponentKitTests/TransactionalDataSource/CKDataSourceChangesetModificationTests.mm (258 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>
#include <stdlib.h>
#import <ComponentKit/CKComponent.h>
#import <ComponentKit/CKCompositeComponent.h>
#import <ComponentKit/CKComponentLayout.h>
#import <ComponentKit/CKComponentProvider.h>
#import <ComponentKit/CKDataSourceAppliedChanges.h>
#import <ComponentKit/CKDataSourceChange.h>
#import <ComponentKit/CKDataSourceChangeset.h>
#import <ComponentKit/CKDataSourceItem.h>
#import <ComponentKit/CKDataSourceChangesetModification.h>
#import <ComponentKit/CKDataSourceState.h>
#import <ComponentKitTestHelpers/CKLifecycleTestComponent.h>
#import <ComponentKitTestHelpers/NSIndexSetExtensions.h>
#import "CKDataSourceStateTestHelpers.h"
static NSString *const kTestModelForLifecycleComponent = @"kTestModelForLifecycleComponent";
@interface CKModelExposingComponent : CKCompositeComponent
+ (instancetype)newWithModel:(id)model;
@property (nonatomic, strong, readonly) id model;
@property (nonatomic, strong, readonly) CKLifecycleTestComponent *lifecycleComponent;
@end
@implementation CKModelExposingComponent
+ (instancetype)newWithModel:(id)model
{
CKLifecycleTestComponent *lifecycleComponent = [model isEqual:kTestModelForLifecycleComponent]
? [CKLifecycleTestComponent new]
: nil;
const auto c = [super newWithComponent:lifecycleComponent ?: CK::ComponentBuilder()
.build()];
if (c) {
c->_model = model;
c->_lifecycleComponent = lifecycleComponent;
}
return c;
}
@end
@interface CKDataSourceChangesetModificationTests : XCTestCase
@end
@implementation CKDataSourceChangesetModificationTests
static CKComponent *ComponentProvider(id<NSObject> model, id<NSObject>)
{
return [CKModelExposingComponent newWithModel:model];
}
- (void)testAppliedChangesIncludesUserInfo
{
CKDataSourceState *originalState = CKDataSourceTestState(ComponentProvider, nil, 1, 1);
NSDictionary *userInfo = @{@"foo": @"bar"};
CKDataSourceChangesetModification *changesetModification =
[[CKDataSourceChangesetModification alloc] initWithChangeset:[[CKDataSourceChangesetBuilder dataSourceChangeset] build]
stateListener:nil
userInfo:userInfo
qos:CKDataSourceQOSDefault];
CKDataSourceChange *change = [changesetModification changeFromState:originalState];
XCTAssertEqualObjects([[change appliedChanges] userInfo], userInfo);
}
- (void)testInsertingSectionAndItemsInEmptyStateExposesNewItems
{
CKDataSourceState *originalState = CKDataSourceTestState(ComponentProvider, nil, 0, 0);
CKDataSourceChangeset *changeset =
[[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withInsertedSections:[NSIndexSet indexSetWithIndex:0]]
withInsertedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @1, [NSIndexPath indexPathForItem:1 inSection:0]: @2}]
build];
CKDataSourceChangesetModification *changesetModification =
[[CKDataSourceChangesetModification alloc] initWithChangeset:changeset
stateListener:nil
userInfo:nil
qos:CKDataSourceQOSDefault];
CKDataSourceChange *change = [changesetModification changeFromState:originalState];
XCTAssertEqual([[change state] numberOfSections], (NSUInteger)1);
XCTAssertEqual([[change state] numberOfObjectsInSection:0], (NSUInteger)2);
}
- (void)testAppliesRemovedItemsThenRemovedSections
{
CKDataSourceState *originalState = CKDataSourceTestState(ComponentProvider, nil, 2, 2);
CKDataSourceChangeset *changeset =
[[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withRemovedItems:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:1]]]
withRemovedSections:[NSIndexSet indexSetWithIndex:0]]
build];
CKDataSourceChangesetModification *changesetModification =
[[CKDataSourceChangesetModification alloc] initWithChangeset:changeset
stateListener:nil
userInfo:nil
qos:CKDataSourceQOSDefault];
CKDataSourceChange *change = [changesetModification changeFromState:originalState];
// Initial state: [0, 1], [2, 3]
// Remove section 0 and item 0 in section 1.
// Result should be [3]
// If items were removed *after* section removals instead of before, we'd have an out-of-range section.
XCTAssertEqual([[change state] numberOfSections], (NSUInteger)1);
XCTAssertEqual([[change state] numberOfObjectsInSection:0], (NSUInteger)1);
auto c = (CKModelExposingComponent *)[[[change state] objectAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]] rootLayout].component();
XCTAssertEqualObjects(c.model, @3);
}
- (void)testUpdateGeneratesNewComponent
{
CKDataSourceState *originalState = CKDataSourceTestState(ComponentProvider, nil, 1, 1);
CKDataSourceChangeset *changeset =
[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withUpdatedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: @"updated"}]
build];
CKDataSourceChangesetModification *changesetModification =
[[CKDataSourceChangesetModification alloc] initWithChangeset:changeset
stateListener:nil
userInfo:nil
qos:CKDataSourceQOSDefault];
CKDataSourceChange *change = [changesetModification changeFromState:originalState];
auto c = (CKModelExposingComponent *)[[[change state] objectAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]] rootLayout].component();
XCTAssertEqualObjects(c.model, @"updated");
}
- (void)testUpdateReturnsInvalidComponentControllers
{
const auto originalState = CKDataSourceTestState(ComponentProvider, nil, 1, 0);
const auto ip = [NSIndexPath indexPathForItem:0 inSection:0];
auto changeset =
[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withInsertedItems:@{ip: kTestModelForLifecycleComponent}]
build];
auto change =
[[[CKDataSourceChangesetModification alloc]
initWithChangeset:changeset
stateListener:nil
userInfo:nil
qos:CKDataSourceQOSDefault]
changeFromState:originalState];
const auto componentController =
((CKModelExposingComponent *)[[change.state objectAtIndexPath:ip] rootLayout].component()).lifecycleComponent.controller;
changeset =
[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withUpdatedItems:@{ip: @""}]
build];
change =
[[[CKDataSourceChangesetModification alloc]
initWithChangeset:changeset
stateListener:nil
userInfo:nil
qos:CKDataSourceQOSDefault]
changeFromState:change.state];
XCTAssertEqual(change.invalidComponentControllers.firstObject, componentController,
@"Invalid component controller should be returned because component is removed from hierarchy.");
}
- (void)testAppliesRemovedItemsThenInsertedItems
{
CKDataSourceState *originalState = CKDataSourceTestState(ComponentProvider, nil, 1, 2);
CKDataSourceChangeset *changeset =
[[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withInsertedItems:@{[NSIndexPath indexPathForItem:1 inSection:0]: @2}]
withRemovedItems:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]]
build];
CKDataSourceChangesetModification *changesetModification =
[[CKDataSourceChangesetModification alloc] initWithChangeset:changeset
stateListener:nil
userInfo:nil
qos:CKDataSourceQOSDefault];
CKDataSourceChange *change = [changesetModification changeFromState:originalState];
// Initial state: [0, 1]
// Remove 0, insert @2 at index 1.
// Result should be [1, 2]
// If removals were applied after insertions, we'd end up with [2, 1] instead.
auto c = (CKModelExposingComponent *)[[[change state] objectAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]] rootLayout].component();
XCTAssertEqualObjects(c.model, @2);
}
- (void)testMoveItem
{
CKDataSourceState *originalState = CKDataSourceTestState(ComponentProvider, nil, 1, 3);
CKDataSourceChangeset *changeset =
[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withMovedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: [NSIndexPath indexPathForItem:2 inSection:0]}]
build];
CKDataSourceChangesetModification *changesetModification =
[[CKDataSourceChangesetModification alloc] initWithChangeset:changeset
stateListener:nil
userInfo:nil
qos:CKDataSourceQOSDefault];
CKDataSourceChange *change = [changesetModification changeFromState:originalState];
// Initial state: [0, 1, 2]
// We move the first element to the last position
// Result should be [1, 2, 0]
auto c = (CKModelExposingComponent *)[[[change state] objectAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]] rootLayout].component();
XCTAssertEqualObjects(c.model, @1);
c = (CKModelExposingComponent *)[[[change state] objectAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]] rootLayout].component();
XCTAssertEqualObjects(c.model, @2);
c = (CKModelExposingComponent *)[[[change state] objectAtIndexPath:[NSIndexPath indexPathForItem:2 inSection:0]] rootLayout].component();
XCTAssertEqualObjects(c.model, @0);
}
- (void)testSwapItemsWithMoves
{
CKDataSourceState *originalState = CKDataSourceTestState(ComponentProvider, nil, 2, 2);
CKDataSourceChangeset *changeset =
[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withMovedItems:@{[NSIndexPath indexPathForItem:0 inSection:0]: [NSIndexPath indexPathForItem:0 inSection:1],
[NSIndexPath indexPathForItem:0 inSection:1]: [NSIndexPath indexPathForItem:0 inSection:0]}]
build];
CKDataSourceChangesetModification *changesetModification =
[[CKDataSourceChangesetModification alloc] initWithChangeset:changeset
stateListener:nil
userInfo:nil
qos:CKDataSourceQOSDefault];
CKDataSourceChange *change = [changesetModification changeFromState:originalState];
// Initial state: [0, 1], [2, 3]
// We should basically swap the first elements in each section;
// Result should be [2, 1], [0, 3]
// If moves were applied immediately one-by-one instead of being modeled as batched removals + inserts,
// then we'd basically just move 0 into section 1 and then back, ending with the same state.
auto c = (CKModelExposingComponent *)[[[change state] objectAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]] rootLayout].component();
XCTAssertEqualObjects(c.model, @2);
c = (CKModelExposingComponent *)[[[change state] objectAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]] rootLayout].component();
XCTAssertEqualObjects(c.model, @0);
}
- (void)testMoveWithRemovals
{
CKDataSourceState *originalState = CKDataSourceTestState(ComponentProvider, nil, 1, 4);
CKDataSourceChangeset *changeset =
[[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withMovedItems:@{[NSIndexPath indexPathForItem:3 inSection:0] : [NSIndexPath indexPathForItem:0 inSection:0] }]
withRemovedItems:[NSSet setWithArray:@[[NSIndexPath indexPathForItem:1 inSection:0], [NSIndexPath indexPathForItem:2 inSection:0]]]]
build];
CKDataSourceChangesetModification *changesetModification =
[[CKDataSourceChangesetModification alloc] initWithChangeset:changeset
stateListener:nil
userInfo:nil
qos:CKDataSourceQOSDefault];
CKDataSourceChange *change = [changesetModification changeFromState:originalState];
// Initial state: [0, 1, 2, 3], Final state: [3, 0]
auto c0 = (CKModelExposingComponent *)[[[change state] objectAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]] rootLayout].component();
auto c1 = (CKModelExposingComponent *)[[[change state] objectAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]] rootLayout].component();
XCTAssertEqualObjects(c0.model, @3);
XCTAssertEqualObjects(c1.model, @0);
}
@end
// Based on https://developer.apple.com/documentation/foundation/nsmutablearray/1416482-insertobjects?language=objc
@interface CKArrayInsertionValidation: XCTestCase
@end
@implementation CKArrayInsertionValidation
- (void)test_WhenInsertionLocationIsCount_IsValid
{
const auto array = @[];
const auto indexes = [NSIndexSet indexSetWithIndex:array.count];
XCTAssertEqual(CK::invalidIndexesForInsertionInArray(array, indexes).count, 0);
}
- (void)test_WhenFirstInsertionLocationIsGreaterThanCount_IsNotValid
{
const auto array = @[@"one"];
const auto indexes = [NSIndexSet indexSetWithIndex:array.count + 1];
XCTAssertEqualObjects(CK::invalidIndexesForInsertionInArray(array, indexes), indexes);
}
- (void)test_WhenOtherInsertionLocationIsGreaterThanCount_IsNotValid
{
const auto array = @[@"one"];
const auto indexes = CK::makeIndexSet({1, 3});
XCTAssertEqualObjects(CK::invalidIndexesForInsertionInArray(array, indexes), CK::makeIndexSet({3}));
}
@end
@interface CKArrayRemovalValidation: XCTestCase
@end
@implementation CKArrayRemovalValidation
- (void)test_WhenRemovalLocationIsEqualToCount_IsNotValid
{
const auto array = @[];
const auto indexes = [NSIndexSet indexSetWithIndex:array.count];
XCTAssertEqualObjects(CK::invalidIndexesForRemovalFromArray(array, indexes), CK::makeIndexSet({0}));
}
- (void)test_WhenFirstRemovalLocationIsWithinBounds_IsValid
{
const auto array = @[@"one"];
const auto indexes = CK::makeIndexSet({0});
XCTAssertEqual(CK::invalidIndexesForRemovalFromArray(array, indexes).count, 0);
}
- (void)test_WhenOtherRemovalLocationIsGreaterThanCount_IsNotValid
{
const auto array = @[@"one"];
const auto indexes = CK::makeIndexSet({0, 1});
XCTAssertEqualObjects(CK::invalidIndexesForRemovalFromArray(array, indexes), CK::makeIndexSet({1}));
}
@end