ComponentKitTests/TransactionalDataSource/CKDataSourceSplitChangesetTests.mm (646 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/CKComponent.h>
#import <ComponentKit/CKComponentProvider.h>
#import <ComponentKit/CKDataSource.h>
#import <ComponentKit/CKDataSourceInternal.h>
#import <ComponentKit/CKDataSourceItem.h>
#import <ComponentKit/CKDataSourceAppliedChanges.h>
#import <ComponentKit/CKDataSourceChangeset.h>
#import <ComponentKit/CKDataSourceConfigurationInternal.h>
#import <ComponentKit/CKDataSourceListener.h>
#import <ComponentKit/CKDataSourceState.h>
#import <ComponentKitTestHelpers/CKTestRunLoopRunning.h>
@interface CKDataSourceSplitChangesetTests : XCTestCase <CKDataSourceListener>
@end
@implementation CKDataSourceSplitChangesetTests {
NSMutableArray<CKDataSourceAppliedChanges *> *_announcedChanges;
CKDataSourceState *_currentDataSourceState;
}
static CKComponent *componentProvider(id<NSObject> model, id<NSObject> _)
{
const CGSize size = [(NSValue *)model CGSizeValue];
return CK::ComponentBuilder()
.width(size.width)
.height(size.height)
.build();
}
- (void)setUp
{
[super setUp];
_announcedChanges = [NSMutableArray<CKDataSourceAppliedChanges *> new];
_currentDataSourceState = nil;
}
- (void)testDataSourceDoesNotSplitChangesetIfChangesetSplittingDisabled
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(10, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 10)), _announcedChanges.firstObject);
}
- (void)testDataSourceDoesNotSplitChangesetIfDoesntFillViewportVertical
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(1, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 1)), _announcedChanges.firstObject);
}
- (void)testDataSourceDoesNotSplitChangesetIfFillsViewportVertical
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(2, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 2)), _announcedChanges.firstObject);
}
- (void)testDataSourceSplitsChangesetIfOverflowsViewportVertical
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 2)), _announcedChanges[0]);
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(2, 2)), _announcedChanges[1]);
}
- (void)testSplitChangesetIsAppliedAsynchronously
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertEqual(1, _announcedChanges.count);
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
}
- (void)testDataSourceDoesNotSplitChangesetIfDoesntFillViewportHorizontal
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 20, .height = 10 },
.layoutAxis = CKDataSourceLayoutAxisHorizontal,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(1, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 1)), _announcedChanges.firstObject);
}
- (void)testDataSourceDoesNotSplitChangesetIfFillsViewportHorizontal
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 20, .height = 10 },
.layoutAxis = CKDataSourceLayoutAxisHorizontal,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(2, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 2)), _announcedChanges.firstObject);
}
- (void)testDataSourceSplitsChangesetIfOverflowsViewportHorizontal
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 20, .height = 10 },
.layoutAxis = CKDataSourceLayoutAxisHorizontal,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 2)), _announcedChanges[0]);
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(2, 2)), _announcedChanges[1]);
}
- (void)testDeferredChangesetHasTheSameUserInfo
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 20, .height = 10 },
.layoutAxis = CKDataSourceLayoutAxisHorizontal,
});
[dataSource addListener:self];
NSDictionary<NSString *, id> *const userInfo = @{@"foo": @YES};
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:userInfo];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
XCTAssertEqualObjects(userInfo, _announcedChanges[0].userInfo);
XCTAssertEqualObjects(userInfo, _announcedChanges[1].userInfo);
}
- (void)testDataSourceDoesNotSplitChangesetIfExistingContentFillsViewport
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
[dataSource applyChangeset:tailInsertionChangeset(NSMakeRange(4, 4), {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertEqual(3, _announcedChanges.count);
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(4, 4)), _announcedChanges[2]);
}
- (void)testDataSourceDoesNotSplitChangesetIfUpdateCausesSizeIncrease
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(1, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
CKDataSourceChangeset *const updateChangeset =
[[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withUpdatedItems:@{
[NSIndexPath indexPathForRow:0 inSection:0]: sizeValue(10, 30),
}]
withInsertedItems:@{
[NSIndexPath indexPathForRow:1 inSection:0]: sizeValue(10, 10),
}]
build];
[dataSource applyChangeset:updateChangeset mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertEqual(2, _announcedChanges.count);
XCTAssertEqualObjects([NSSet setWithObject:[NSIndexPath indexPathForRow:0 inSection:0]], _announcedChanges[1].updatedIndexPaths);
XCTAssertEqualObjects([NSSet setWithObject:[NSIndexPath indexPathForRow:1 inSection:0]], _announcedChanges[1].insertedIndexPaths);
}
- (void)testDataSourceSplitsChangesetIfUpdateCausesSizeDecrease
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(1, {.width = 10, .height = 40}) mode:CKUpdateModeSynchronous userInfo:nil];
CKDataSourceChangeset *const updateChangeset =
[[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withUpdatedItems:@{
[NSIndexPath indexPathForRow:0 inSection:0]: sizeValue(10, 10),
}]
withInsertedItems:@{
[NSIndexPath indexPathForRow:1 inSection:0]: sizeValue(10, 10),
[NSIndexPath indexPathForRow:2 inSection:0]: sizeValue(10, 10),
}]
build];
[dataSource applyChangeset:updateChangeset mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 3;
}));
XCTAssertEqualObjects([NSSet setWithObject:[NSIndexPath indexPathForRow:0 inSection:0]], _announcedChanges[1].updatedIndexPaths);
XCTAssertEqualObjects([NSSet setWithObject:[NSIndexPath indexPathForRow:1 inSection:0]], _announcedChanges[1].insertedIndexPaths);
XCTAssertEqualObjects([NSSet setWithObject:[NSIndexPath indexPathForRow:2 inSection:0]], _announcedChanges[2].insertedIndexPaths);
}
- (void)testDataSourceSplitsChangesetIfItemRemovalCausesSizeDecrease
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(1, {.width = 10, .height = 40}) mode:CKUpdateModeSynchronous userInfo:nil];
CKDataSourceChangeset *const removalChangeset =
[[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withRemovedItems:[NSSet setWithObject:[NSIndexPath indexPathForRow:0 inSection:0]]]
withInsertedItems:@{
[NSIndexPath indexPathForRow:0 inSection:0]: sizeValue(10, 20),
[NSIndexPath indexPathForRow:1 inSection:0]: sizeValue(10, 20),
}]
build];
[dataSource applyChangeset:removalChangeset mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 3;
}));
XCTAssertEqualObjects([NSSet setWithObject:[NSIndexPath indexPathForRow:0 inSection:0]], _announcedChanges[1].removedIndexPaths);
XCTAssertEqualObjects([NSSet setWithObject:[NSIndexPath indexPathForRow:0 inSection:0]], _announcedChanges[1].insertedIndexPaths);
XCTAssertEqualObjects([NSSet setWithObject:[NSIndexPath indexPathForRow:1 inSection:0]], _announcedChanges[2].insertedIndexPaths);
}
- (void)testDataSourceDoesNotSplitChangesetsIfNotContiguousTailInsertion
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(1, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
[dataSource applyChangeset:tailInsertionChangeset(NSMakeRange(0, 2), {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertEqual(2, _announcedChanges.count);
XCTAssertEqualObjects([NSSet setWithObject:[NSIndexPath indexPathForRow:0 inSection:0]], _announcedChanges[0].insertedIndexPaths);
NSSet<NSIndexPath *> *const expectedInsertedIndexPaths = [NSSet setWithObjects:[NSIndexPath indexPathForRow:0 inSection:0], [NSIndexPath indexPathForRow:1 inSection:0], nil];
XCTAssertEqualObjects(expectedInsertedIndexPaths, _announcedChanges[1].insertedIndexPaths);
}
- (void)testDataSourceDoesNotSplitUpdateChangesetsWhenOptionDisabled
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
[dataSource applyChangeset:updateChangeset(NSMakeRange(0, 4), {.width = 10, .height = 20}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertEqual(3, _announcedChanges.count);
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 2)), _announcedChanges[0]);
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(2, 2)), _announcedChanges[1]);
XCTAssertEqualObjects(expectedAppliedChangesForUpdate(NSMakeRange(0, 4)), _announcedChanges[2]);
}
- (void)testDataSourceSplitsUpdateChangesetsWhenOptionEnabled
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.splitUpdates = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
[dataSource applyChangeset:updateChangeset(NSMakeRange(0, 4), {.width = 10, .height = 20}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 4;
}));
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 2)), _announcedChanges[0]);
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(2, 2)), _announcedChanges[1]);
XCTAssertEqualObjects(expectedAppliedChangesForUpdate(NSMakeRange(0, 1)), _announcedChanges[2]);
XCTAssertEqualObjects(expectedAppliedChangesForUpdate(NSMakeRange(1, 3)), _announcedChanges[3]);
}
- (void)testDataSourceSplitsChangesetCorrectlyForNonZeroOffset
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.splitUpdates = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource setViewport:{
.contentOffset = { .x = 0, .y = 10 },
}];
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
[dataSource applyChangeset:updateChangeset(NSMakeRange(0, 4), {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 4;
}));
CKDataSourceAppliedChanges *const expectedAppliedChanges1 =
[[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:nil
removedIndexPaths:nil
removedSections:nil
movedIndexPaths:nil
insertedSections:[NSIndexSet indexSetWithIndex:0]
insertedIndexPaths:[NSSet setWithObjects:[NSIndexPath indexPathForItem:0 inSection:0], [NSIndexPath indexPathForItem:1 inSection:0], [NSIndexPath indexPathForItem:2 inSection:0], nil]
userInfo:nil];
XCTAssertEqualObjects(expectedAppliedChanges1, _announcedChanges[0]);
CKDataSourceAppliedChanges *const expectedAppliedChanges2 =
[[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:nil
removedIndexPaths:nil
removedSections:nil
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:3 inSection:0]]
userInfo:nil];
XCTAssertEqualObjects(expectedAppliedChanges2, _announcedChanges[1]);
CKDataSourceAppliedChanges *const expectedAppliedChanges3 =
[[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:[NSSet setWithObjects:[NSIndexPath indexPathForItem:1 inSection:0], [NSIndexPath indexPathForItem:2 inSection:0], nil]
removedIndexPaths:nil
removedSections:nil
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:nil
userInfo:nil];
XCTAssertEqualObjects(expectedAppliedChanges3, _announcedChanges[2]);
CKDataSourceAppliedChanges *const expectedAppliedChanges4 =
[[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:[NSSet setWithObjects:[NSIndexPath indexPathForItem:0 inSection:0], [NSIndexPath indexPathForItem:3 inSection:0], nil]
removedIndexPaths:nil
removedSections:nil
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:nil
userInfo:nil];
XCTAssertEqualObjects(expectedAppliedChanges4, _announcedChanges[3]);
}
- (void)testDataSourceDoesNotProcessDeferredUpdateWhenItemIsRemoved
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.splitUpdates = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
CKDataSourceChangeset *const updateWithRemovalChangeset =
[[[[CKDataSourceChangesetBuilder
dataSourceChangeset]
withUpdatedItems:@{[NSIndexPath indexPathForItem:3 inSection:0]: [NSValue valueWithCGSize:{.width = 10, .height = 10}]}]
withRemovedItems:[NSSet setWithObject:[NSIndexPath indexPathForItem:3 inSection:0]]]
build];
[dataSource applyChangeset:updateWithRemovalChangeset mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 3;
}));
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 2)), _announcedChanges[0]);
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(2, 2)), _announcedChanges[1]);
CKDataSourceAppliedChanges *expectedRemovalChanges =
[[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:nil
removedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:3 inSection:0]]
removedSections:nil
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:nil
userInfo:nil];
XCTAssertEqualObjects(expectedRemovalChanges, _announcedChanges[2]);
}
- (void)testDataSourceDoesNotProcessDeferredUpdateWhenSectionIsRemoved
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.splitUpdates = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
CKDataSourceChangeset *const updateWithRemovalChangeset =
[[[[CKDataSourceChangesetBuilder
dataSourceChangeset]
withUpdatedItems:@{[NSIndexPath indexPathForItem:3 inSection:0]: [NSValue valueWithCGSize:{.width = 10, .height = 10}]}]
withRemovedSections:[NSIndexSet indexSetWithIndex:0]]
build];
[dataSource applyChangeset:updateWithRemovalChangeset mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 3;
}));
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 2)), _announcedChanges[0]);
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(2, 2)), _announcedChanges[1]);
CKDataSourceAppliedChanges *expectedRemovalChanges =
[[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:nil
removedIndexPaths:nil
removedSections:[NSIndexSet indexSetWithIndex:0]
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:nil
userInfo:nil];
XCTAssertEqualObjects(expectedRemovalChanges, _announcedChanges[2]);
}
- (void)testDataSourceShiftsUpdatedChangesetIndicesWhenItemsAreRemoved
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.splitUpdates = YES,
.viewportBoundingSize = { .width = 10, .height = 20 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
[dataSource applyChangeset:initialInsertionChangeset(4, {.width = 10, .height = 10}) mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
CKDataSourceChangeset *const updateWithRemovalChangeset =
[[[[CKDataSourceChangesetBuilder
dataSourceChangeset]
withUpdatedItems:@{[NSIndexPath indexPathForItem:2 inSection:0]: [NSValue valueWithCGSize:{.width = 10, .height = 10}]}]
withRemovedItems:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]]
build];
[dataSource applyChangeset:updateWithRemovalChangeset mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 4;
}));
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(0, 2)), _announcedChanges[0]);
XCTAssertEqualObjects(expectedAppliedChangesForInsertion(NSMakeRange(2, 2)), _announcedChanges[1]);
CKDataSourceAppliedChanges *const expectedRemovalChanges =
[[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:nil
removedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]
removedSections:nil
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:nil
userInfo:nil];
XCTAssertEqualObjects(expectedRemovalChanges, _announcedChanges[2]);
CKDataSourceAppliedChanges *const expectedUpdateChanges =
[[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:1 inSection:0]]
removedIndexPaths:nil
removedSections:nil
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:nil
userInfo:nil];
XCTAssertEqualObjects(expectedUpdateChanges, _announcedChanges[3]);
}
- (void)testDataSourceShiftsUpdatedChangesetIndicesWhenSectionsAreRemoved
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.splitUpdates = YES,
.viewportBoundingSize = { .width = 10, .height = 10 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
CKDataSourceChangeset *const insertionChangeset =
[[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withInsertedSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]]
withInsertedItems:@{
[NSIndexPath indexPathForItem:0 inSection:0]: [NSValue valueWithCGSize:{.width = 10, .height = 10}],
[NSIndexPath indexPathForItem:0 inSection:1]: [NSValue valueWithCGSize:{.width = 10, .height = 10}]
}]
build];
[dataSource applyChangeset:insertionChangeset mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
CKDataSourceChangeset *const updateWithRemovalChangeset =
[[[[CKDataSourceChangesetBuilder
dataSourceChangeset]
withUpdatedItems:@{[NSIndexPath indexPathForItem:0 inSection:1]: [NSValue valueWithCGSize:{.width = 10, .height = 10}]}]
withRemovedSections:[NSIndexSet indexSetWithIndex:0]]
build];
[dataSource applyChangeset:updateWithRemovalChangeset mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 4;
}));
CKDataSourceAppliedChanges *const expectedRemovalChanges =
[[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:nil
removedIndexPaths:nil
removedSections:[NSIndexSet indexSetWithIndex:0]
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:nil
userInfo:nil];
XCTAssertEqualObjects(expectedRemovalChanges, _announcedChanges[2]);
CKDataSourceAppliedChanges *const expectedUpdateChanges =
[[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:[NSSet setWithObject:[NSIndexPath indexPathForItem:0 inSection:0]]
removedIndexPaths:nil
removedSections:nil
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:nil
userInfo:nil];
XCTAssertEqualObjects(expectedUpdateChanges, _announcedChanges[3]);
}
- (void)testDataSourceStateAfterApplyingSplitUpdateChangesetContainsCorrectModels
{
CKDataSource *const dataSource = dataSourceWithSplitChangesetOptions(componentProvider, {
.enabled = YES,
.splitUpdates = YES,
.viewportBoundingSize = { .width = 10, .height = 10 },
.layoutAxis = CKDataSourceLayoutAxisVertical,
});
[dataSource addListener:self];
CKDataSourceChangeset *const insertionChangeset =
[[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withInsertedSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]]
withInsertedItems:@{
[NSIndexPath indexPathForItem:0 inSection:0]: [NSValue valueWithCGSize:{.width = 10, .height = 10}],
[NSIndexPath indexPathForItem:0 inSection:1]: [NSValue valueWithCGSize:{.width = 10, .height = 10}]
}]
build];
[dataSource applyChangeset:insertionChangeset mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 2;
}));
CKDataSourceChangeset *const updateWithInsertionChangeset =
[[[[CKDataSourceChangesetBuilder
dataSourceChangeset]
withInsertedItems:@{[NSIndexPath indexPathForItem:1 inSection:1]: [NSValue valueWithCGSize:{.width = 10, .height = 10}]}]
withUpdatedItems:@{[NSIndexPath indexPathForItem:0 inSection:1]: [NSValue valueWithCGSize:{.width = 10, .height = 10}]}]
build];
[dataSource applyChangeset:updateWithInsertionChangeset mode:CKUpdateModeSynchronous userInfo:nil];
XCTAssertTrue(CKRunRunLoopUntilBlockIsTrue(^BOOL{
return _announcedChanges.count == 4;
}));
NSValue *const expectedModel = [NSValue valueWithCGSize:{.width = 10, .height = 10}];
[_currentDataSourceState enumerateObjectsUsingBlock:^(CKDataSourceItem *item, NSIndexPath *indexPath, BOOL *stop) {
XCTAssertEqualObjects(expectedModel, item.model);
}];
}
static CKDataSource *dataSourceWithSplitChangesetOptions(CKComponentProviderFunc componentProvider, const CKDataSourceSplitChangesetOptions &splitChangesetOptions)
{
CKDataSourceConfiguration *const config =
[[CKDataSourceConfiguration alloc]
initWithComponentProviderFunc:componentProvider
context:nil
sizeRange:{}
options:{.splitChangesetOptions = splitChangesetOptions}
componentPredicates:{}
componentControllerPredicates:{}
analyticsListener:nil];
return [[CKDataSource alloc] initWithConfiguration:config];
}
static CKDataSourceChangeset *initialInsertionChangeset(NSUInteger itemCount, CGSize size)
{
NSMutableDictionary<NSIndexPath *, NSValue *> *const items = [NSMutableDictionary<NSIndexPath *, NSValue *> dictionaryWithCapacity:itemCount];
for (NSUInteger i = 0; i < itemCount; i++) {
items[[NSIndexPath indexPathForItem:i inSection:0]] = [NSValue valueWithCGSize:size];
}
return [[[[CKDataSourceChangesetBuilder dataSourceChangeset]
withInsertedSections:[NSIndexSet indexSetWithIndex:0]]
withInsertedItems:items]
build];
}
static CKDataSourceChangeset *tailInsertionChangeset(NSRange range, CGSize size)
{
NSMutableDictionary<NSIndexPath *, NSValue *> *const items = [NSMutableDictionary<NSIndexPath *, NSValue *> dictionaryWithCapacity:range.length];
for (NSUInteger i = 0; i < range.length; i++) {
items[[NSIndexPath indexPathForItem:i + range.location inSection:0]] = [NSValue valueWithCGSize:size];
}
return [[[CKDataSourceChangesetBuilder dataSourceChangeset] withInsertedItems:items] build];
}
static CKDataSourceChangeset *updateChangeset(NSRange range, CGSize size)
{
NSMutableDictionary<NSIndexPath *, NSValue *> *const items = [NSMutableDictionary<NSIndexPath *, NSValue *> dictionaryWithCapacity:range.length];
for (NSUInteger i = 0; i < range.length; i++) {
items[[NSIndexPath indexPathForItem:i + range.location inSection:0]] = [NSValue valueWithCGSize:size];
}
return [[[CKDataSourceChangesetBuilder dataSourceChangeset] withUpdatedItems:items] build];
}
static CKDataSourceAppliedChanges *expectedAppliedChangesForInsertion(NSRange range)
{
NSIndexSet *insertedSections = nil;
if (range.location == 0) {
insertedSections = [NSIndexSet indexSetWithIndex:0];
}
NSMutableSet<NSIndexPath *> *const insertedIndexPaths = [NSMutableSet<NSIndexPath *> setWithCapacity:range.length];
for (NSUInteger i = 0; i < range.length; i++) {
[insertedIndexPaths addObject:[NSIndexPath indexPathForItem:range.location + i inSection:0]];
}
return [[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:nil
removedIndexPaths:nil
removedSections:nil
movedIndexPaths:nil
insertedSections:insertedSections
insertedIndexPaths:insertedIndexPaths
userInfo:nil];
}
static CKDataSourceAppliedChanges *expectedAppliedChangesForUpdate(NSRange range)
{
NSMutableSet<NSIndexPath *> *const updatedIndexPaths = [NSMutableSet<NSIndexPath *> setWithCapacity:range.length];
for (NSUInteger i = 0; i < range.length; i++) {
[updatedIndexPaths addObject:[NSIndexPath indexPathForItem:range.location + i inSection:0]];
}
return [[CKDataSourceAppliedChanges alloc]
initWithUpdatedIndexPaths:updatedIndexPaths
removedIndexPaths:nil
removedSections:nil
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:nil
userInfo:nil];
}
static NSValue *sizeValue(CGFloat width, CGFloat height)
{
return [NSValue valueWithCGSize:{.width = width, .height = height}];
}
#pragma mark - CKDataSourceListener
- (void)dataSource:(CKDataSource *)dataSource
didModifyPreviousState:(CKDataSourceState *)previousState
withState:(CKDataSourceState *)state
byApplyingChanges:(CKDataSourceAppliedChanges *)changes
{
[_announcedChanges addObject:changes];
_currentDataSourceState = state;
}
- (void)dataSource:(CKDataSource *)dataSource
willApplyDeferredChangeset:(CKDataSourceChangeset *)deferredChangeset {}
@end