IGListKit/Tests/IGListBindingSectionControllerTests.m
Ryan Nystrom a15ea08614 Move section controller mutation API to object provided in update block
Summary:
We constantly have random bugs pop up when mutations are made outside of a batch update. This change restricts the mutation API to a batch context object (which is just an `IGListAdapter`) so they can only be done **inside an update block**.

- Fixed open source project
- Confirmed open source examples build
- Updated all of Instagram.app to use this API
- Changelog breaking changes entry

Fixes #392

Reviewed By: jessesquires

Differential Revision: D4754129

fbshipit-source-id: 11d32a0fac3e50c9edbb01e92a8a0c7b8a43cf2d
2017-03-22 12:46:49 -07:00

247 lines
11 KiB
Objective-C

/**
* Copyright (c) 2016-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 <IGListKit/IGListKit.h>
#import "IGTestDiffingDataSource.h"
#import "IGTestDiffingObject.h"
#import "IGTestDiffingSectionController.h"
#import "IGTestStringBindableCell.h"
#import "IGTestNumberBindableCell.h"
#import "IGListAdapterInternal.h"
#import "IGTestObject.h"
#import "IGTestCell.h"
@interface IGListBindingSectionControllerTests : XCTestCase
@property (nonatomic, strong) IGListCollectionView *collectionView;
@property (nonatomic, strong) IGListAdapter *adapter;
@property (nonatomic, strong) IGTestDiffingDataSource *dataSource;
@property (nonatomic, strong) UICollectionViewFlowLayout *layout;
@property (nonatomic, strong) UIWindow *window;
@end
@implementation IGListBindingSectionControllerTests
- (void)setUp {
[super setUp];
self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 1000)];
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
self.collectionView = [[IGListCollectionView alloc] initWithFrame:self.window.bounds collectionViewLayout:layout];
[self.window addSubview:self.collectionView];
self.dataSource = [IGTestDiffingDataSource new];
self.adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new]
viewController:nil
workingRangeSize:0];
}
- (void)tearDown {
[super tearDown];
self.window = nil;
self.collectionView = nil;
self.adapter = nil;
self.dataSource = nil;
}
- (void)setupWithObjects:(NSArray<IGTestDiffingObject *> *)objects {
self.dataSource.objects = objects;
self.adapter.collectionView = self.collectionView;
self.adapter.dataSource = self.dataSource;
[self.collectionView layoutIfNeeded];
}
- (id)cellAtSection:(NSInteger)section item:(NSInteger)item {
return [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:item inSection:section]];
}
- (void)test_whenInitialLoad_withEmptyViewModels_thatCollectionViewEmpty {
[self setupWithObjects:@[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[]]
]];
XCTAssertEqual([self.collectionView numberOfSections], 1);
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 0);
}
- (void)test_whenInitialLoad_withMultipleViewModels_thatCellsMappedAndConfigured {
[self setupWithObjects:@[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
[[IGTestDiffingObject alloc] initWithKey:@2 objects:@[@"foo", @"bar", @42]],
[[IGTestDiffingObject alloc] initWithKey:@3 objects:@[]],
]];
XCTAssertEqual([self.collectionView numberOfSections], 3);
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2);
XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3);
XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 0);
IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0];
IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1];
IGTestStringBindableCell *cell10 = [self cellAtSection:1 item:0];
IGTestStringBindableCell *cell11 = [self cellAtSection:1 item:1];
IGTestNumberBindableCell *cell12 = [self cellAtSection:1 item:2];
XCTAssertEqualObjects(cell00.textField.text, @"7");
XCTAssertEqualObjects(cell01.label.text, @"seven");
XCTAssertEqualObjects(cell10.label.text, @"foo");
XCTAssertEqualObjects(cell11.label.text, @"bar");
XCTAssertEqualObjects(cell12.textField.text, @"42");
}
- (void)test_whenUpdating_withAddedModels_thatCellsCorrectAndConfigured {
[self setupWithObjects:@[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
]];
self.dataSource.objects = @[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven", @8, @"eight"]],
];
XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)];
[self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
XCTAssertEqual([self.collectionView numberOfSections], 1);
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 4);
IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0];
IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1];
IGTestNumberBindableCell *cell02 = [self cellAtSection:0 item:2];
IGTestStringBindableCell *cell03 = [self cellAtSection:0 item:3];
XCTAssertEqualObjects(cell00.textField.text, @"7");
XCTAssertEqualObjects(cell01.label.text, @"seven");
XCTAssertEqualObjects(cell02.textField.text, @"8");
XCTAssertEqualObjects(cell03.label.text, @"eight");
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:15 handler:nil];
}
- (void)test_whenSelectingCell_thatCorrectViewModelSelected {
[self setupWithObjects:@[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
]];
[self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]];
IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject];
XCTAssertEqualObjects(section.selectedViewModel, @"seven");
}
- (void)test_whenAdapterReloadsObjects_thatSectionUpdated {
[self setupWithObjects:@[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
]];
[self.adapter reloadObjects:@[[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@"four", @4, @"seven", @7]]]];
IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0];
IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1];
XCTAssertEqualObjects(cell00.textField.text, @"7");
XCTAssertEqualObjects(cell01.label.text, @"seven");
XCTAssertNil([self cellAtSection:0 item:2]);
XCTAssertNil([self cellAtSection:0 item:3]);
// "fake" batch updates to make sure that calling reload triggers a diffed batch update
XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)];
[self.adapter performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext){} completion:^(BOOL finished) {
IGTestStringBindableCell *batchedCell00 = [self cellAtSection:0 item:0];
IGTestNumberBindableCell *batchedCell01 = [self cellAtSection:0 item:1];
IGTestStringBindableCell *batchedCell02 = [self cellAtSection:0 item:2];
IGTestNumberBindableCell *batchedCell03 = [self cellAtSection:0 item:3];
XCTAssertEqualObjects(batchedCell00.label.text, @"four");
XCTAssertEqualObjects(batchedCell01.textField.text, @"4");
XCTAssertEqualObjects(batchedCell02.label.text, @"seven");
XCTAssertEqualObjects(batchedCell03.textField.text, @"7");
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:16 handler:nil];
}
- (void)test_whenUpdating_withViewModelMovesAndReloads_thatCellUpdatedAndInstanceSame {
NSArray *initObjects = @[
@"foo",
@"bar",
[[IGTestObject alloc] initWithKey:@42 value:@"baz"]
];
[self setupWithObjects:@[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:initObjects]
]];
XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3);
IGTestStringBindableCell *cell00 = [self cellAtSection:0 item:0];
IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1];
IGTestCell *cell02 = [self cellAtSection:0 item:2];
XCTAssertEqualObjects(cell00.label.text, @"foo");
XCTAssertEqualObjects(cell01.label.text, @"bar");
XCTAssertEqualObjects(cell02.label.text, @"baz");
NSArray *newObjects = @[
[[IGTestObject alloc] initWithKey:@42 value:@"bang"], // moved to section 0 and value changed
@"foo",
@"bar",
];
self.dataSource.objects = @[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:newObjects]
];
XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)];
[self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
IGTestCell *batchedCell00 = [self cellAtSection:0 item:0];
IGTestStringBindableCell *batchedCell01 = [self cellAtSection:0 item:1];
IGTestStringBindableCell *batchedCell02 = [self cellAtSection:0 item:2];
XCTAssertEqualObjects(batchedCell00.label.text, @"bang");
XCTAssertEqualObjects(batchedCell01.label.text, @"foo");
XCTAssertEqualObjects(batchedCell02.label.text, @"bar");
XCTAssertEqual(cell00, batchedCell01);
XCTAssertEqual(cell01, batchedCell02);
XCTAssertEqual(cell02, batchedCell00);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:16 handler:nil];
}
- (void)test_whenUpdatingManually_with2Updates_thatBothCompletionBlocksCalled {
[self setupWithObjects:@[
[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]],
]];
IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
XCTestExpectation *expectation1 = [self expectationWithDescription:NSStringFromSelector(_cmd)];
[section updateAnimated:YES completion:^(BOOL updated) {
XCTAssertTrue(updated);
[expectation1 fulfill];
}];
XCTestExpectation *expectation2 = [self expectationWithDescription:NSStringFromSelector(_cmd)];
[section updateAnimated:YES completion:^(BOOL updated) {
// queued second, shouldn't execute update block
XCTAssertFalse(updated);
[expectation2 fulfill];
}];
[self waitForExpectationsWithTimeout:15 handler:nil];
}
@end