Add more unit tests to stack section controller

Summary:
Beefing up our test coverage. Made an improvement to supplementary view behavior along the way. Will update `CHANGELOG.md` once travis finishes w/ link to PR #.

- [x] All tests pass. Demo project builds and runs.
- [x] I added tests, an experiment, or detailed why my change isn't tested.
- [x] I have reviewed the [contributing guide](https://github.com/Instagram/IGListKit/blob/master/.github/CONTRIBUTING.md)
Closes https://github.com/Instagram/IGListKit/pull/286

Differential Revision: D4281961

Pulled By: jessesquires

fbshipit-source-id: 32b5877bd72250b9a99e600ceffc64d686fa5651
This commit is contained in:
Ryan Nystrom 2016-12-05 17:38:56 -08:00 committed by Facebook Github Bot
parent 4baf267354
commit 10bdfb23f9
5 changed files with 304 additions and 42 deletions

View file

@ -106,6 +106,10 @@ This release closes the [2.0.0 milestone](https://github.com/Instagram/IGListKit
- Added `tvOS` example pack. [Sherlouk](https://github.com/Sherlouk) [(#141)](https://github.com/Instagram/IGListKit/pull/141)
- Fixed a bug where `IGListStackSectionController` would only set its supplementary source once. [Ryan Nystrom](https://github.com/rnystrom) [(#286)](https://github.com/Instagram/IGListKit/pull/286)
- Fixed a bug where `IGListStackSectionController` passed the wrong section controller for will-drag scroll events. [Ryan Nystrom](https://github.com/rnystrom) [(#286)](https://github.com/Instagram/IGListKit/pull/286)
1.0.0
-----

View file

@ -455,12 +455,12 @@
8285404F1DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.m */,
8240C7F91DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.h */,
8240C7FA1DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.m */,
8285404A1DE40C6E00118B94 /* IGListTestHorizontalSection.h */,
8285404B1DE40C6E00118B94 /* IGListTestHorizontalSection.m */,
88144EF31D870EDC007C7F66 /* IGListTestOffsettingLayout.h */,
88144EF41D870EDC007C7F66 /* IGListTestOffsettingLayout.m */,
88144EF51D870EDC007C7F66 /* IGListTestSection.h */,
88144EF61D870EDC007C7F66 /* IGListTestSection.m */,
8285404A1DE40C6E00118B94 /* IGListTestHorizontalSection.h */,
8285404B1DE40C6E00118B94 /* IGListTestHorizontalSection.m */,
8240C7F61DC2F3FB00B3AAE7 /* IGListTestStoryboardSection.h */,
8240C7F71DC2F3FB00B3AAE7 /* IGListTestStoryboardSection.m */,
88144EF71D870EDC007C7F66 /* IGListTestUICollectionViewDataSource.h */,

View file

@ -16,32 +16,6 @@
#import "IGListSectionControllerInternal.h"
@interface UICollectionViewCell (IGListStackedSectionController)
@end
@implementation UICollectionViewCell (IGListStackedSectionController)
static void * kStackedSectionControllerKey = &kStackedSectionControllerKey;
- (void)ig_setStackedSectionController:(id)stackedSectionController {
objc_setAssociatedObject(self, kStackedSectionControllerKey, stackedSectionController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)ig_stackedSectionController {
return objc_getAssociatedObject(self, kStackedSectionControllerKey);
}
static void * kStackedSectionControllerIndexKey = &kStackedSectionControllerIndexKey;
- (void)ig_setStackedSectionControllerIndex:(NSInteger)stackedSectionControllerIndex {
objc_setAssociatedObject(self, kStackedSectionControllerIndexKey, @(stackedSectionControllerIndex), OBJC_ASSOCIATION_ASSIGN);
}
- (NSInteger)ig_stackedSectionControllerIndex {
return [objc_getAssociatedObject(self, kStackedSectionControllerIndexKey) integerValue];
}
@end
@implementation IGListStackedSectionController
- (instancetype)initWithSectionControllers:(NSArray <IGListSectionController<IGListSectionType> *> *)sectionControllers {
@ -49,10 +23,6 @@ static void * kStackedSectionControllerIndexKey = &kStackedSectionControllerInde
for (IGListSectionController<IGListSectionType> *sectionController in sectionControllers) {
sectionController.collectionContext = self;
sectionController.viewController = self.viewController;
if (self.supplementaryViewSource == nil) {
self.supplementaryViewSource = sectionController.supplementaryViewSource;
}
}
_visibleSectionControllers = [[NSCountedSet alloc] init];
@ -123,6 +93,15 @@ static void * kStackedSectionControllerIndexKey = &kStackedSectionControllerInde
return itemIndexes;
}
- (id<IGListSupplementaryViewSource>)supplementaryViewSource {
for (IGListSectionController *sectionController in self.sectionControllers) {
id<IGListSupplementaryViewSource> supplementaryViewSource = sectionController.supplementaryViewSource;
if (supplementaryViewSource != nil) {
return supplementaryViewSource;
}
}
return nil;
}
#pragma mark - IGListSectionType
@ -305,9 +284,6 @@ static void * kStackedSectionControllerIndexKey = &kStackedSectionControllerInde
IGListSectionController<IGListSectionType> *childSectionController = [self sectionControllerForObjectIndex:index];
const NSUInteger localIndex = [self localIndexForSectionController:childSectionController index:index];
[cell ig_setStackedSectionController:childSectionController];
[cell ig_setStackedSectionControllerIndex:localIndex];
NSCountedSet *visibleSectionControllers = self.visibleSectionControllers;
id<IGListDisplayDelegate> displayDelegate = [childSectionController displayDelegate];
@ -346,7 +322,7 @@ static void * kStackedSectionControllerIndexKey = &kStackedSectionControllerInde
- (void)listAdapter:(IGListAdapter *)listAdapter willBeginDraggingSectionController:(IGListSectionController<IGListSectionType> *)sectionController {
for (IGListSectionController<IGListSectionType> *childSectionController in self.sectionControllers) {
[[childSectionController scrollDelegate] listAdapter:listAdapter willBeginDraggingSectionController:sectionController];
[[childSectionController scrollDelegate] listAdapter:listAdapter willBeginDraggingSectionController:childSectionController];
}
}

View file

@ -17,7 +17,13 @@
#import "IGListDisplayHandler.h"
#import "IGListStackedSectionControllerInternal.h"
#import "IGListTestSection.h"
#import "IGTestCell.h"
#import "IGTestStackedDataSource.h"
#import "IGTestStoryboardCell.h"
#import "IGTestStoryboardViewController.h"
#import "IGTestSupplementarySource.h"
#import "IGTestSupplementarySource.h"
#import "IGTestStoryboardSupplementarySource.h"
static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}};
@ -37,9 +43,15 @@ static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}};
self.window = [[UIWindow alloc] initWithFrame:kStackTestFrame];
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
self.collectionView = [[IGListCollectionView alloc] initWithFrame:kStackTestFrame collectionViewLayout:layout];
[self.window addSubview:self.collectionView];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"IGTestStoryboard" bundle:[NSBundle bundleForClass:self.class]];
IGTestStoryboardViewController *vc = [storyboard instantiateViewControllerWithIdentifier:@"testVC"];
self.window.rootViewController = vc;
[self.window addSubview:vc.view];
[vc performSelectorOnMainThread:@selector(loadView) withObject:nil waitUntilDone:YES];
self.collectionView = vc.collectionView;
vc.view.frame = kStackTestFrame;
self.collectionView.frame = kStackTestFrame;
self.dataSource = [[IGTestStackedDataSource alloc] init];
self.adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil workingRangeSize:0];
@ -431,4 +443,247 @@ static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}};
[self waitForExpectationsWithTimeout:15 handler:nil];
}
- (void)test_whenSelectingItems_thatChildSectionControllersSelected {
[self setupWithObjects:@[
[[IGTestObject alloc] initWithKey:@0 value:@[@1, @2, @3]],
[[IGTestObject alloc] initWithKey:@1 value:@[@1, @2, @3]],
[[IGTestObject alloc] initWithKey:@2 value:@[@1, @1]]
]];
[self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
[self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:[NSIndexPath indexPathForItem:2 inSection:1]];
[self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:2]];
IGListStackedSectionController *stack0 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
IGListStackedSectionController *stack1 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];
IGListStackedSectionController *stack2 = [self.adapter sectionControllerForObject:self.dataSource.objects[2]];
XCTAssertTrue([stack0.sectionControllers[0] wasSelected]);
XCTAssertFalse([stack0.sectionControllers[1] wasSelected]);
XCTAssertFalse([stack0.sectionControllers[2] wasSelected]);
XCTAssertFalse([stack1.sectionControllers[0] wasSelected]);
XCTAssertTrue([stack1.sectionControllers[1] wasSelected]);
XCTAssertFalse([stack1.sectionControllers[2] wasSelected]);
XCTAssertFalse([stack2.sectionControllers[0] wasSelected]);
XCTAssertTrue([stack2.sectionControllers[1] wasSelected]);
}
- (void)test_whenUsingNibs_withStoryboards_thatCellsAreConfigured {
[self setupWithObjects:@[
[[IGTestObject alloc] initWithKey:@0 value:@[@1, @"nib", @"storyboard"]],
]];
UICollectionViewCell *cell0 = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]];
IGTestStoryboardCell *cell2 = (IGTestStoryboardCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:2 inSection:0]];
XCTAssertEqualObjects(cell0.class, [UICollectionViewCell class]);
XCTAssertEqualObjects(cell1.class, [IGTestCell class]);
XCTAssertEqualObjects(cell2.class, [IGTestStoryboardCell class]);
XCTAssertEqualObjects(cell1.label.text, @"nib");
XCTAssertEqualObjects(cell2.label.text, @"storyboard");
}
- (void)test_whenForwardingDidScrollEvent_thatChildSectionControllersReceiveEvent {
[self setupWithObjects:@[
[[IGTestObject alloc] initWithKey:@0 value:@[@1, @2, @3]],
[[IGTestObject alloc] initWithKey:@2 value:@[@1, @1]]
]];
id mockScrollDelegate = [OCMockObject mockForProtocol:@protocol(IGListScrollDelegate)];
IGListStackedSectionController *stack0 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
IGListStackedSectionController *stack1 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];
[stack0.sectionControllers[0] setScrollDelegate:mockScrollDelegate];
[stack0.sectionControllers[1] setScrollDelegate:mockScrollDelegate];
[stack0.sectionControllers[2] setScrollDelegate:mockScrollDelegate];
[stack1.sectionControllers[0] setScrollDelegate:mockScrollDelegate];
[stack1.sectionControllers[1] setScrollDelegate:mockScrollDelegate];
[[mockScrollDelegate expect] listAdapter:self.adapter didScrollSectionController:stack0.sectionControllers[0]];
[[mockScrollDelegate expect] listAdapter:self.adapter didScrollSectionController:stack0.sectionControllers[1]];
[[mockScrollDelegate expect] listAdapter:self.adapter didScrollSectionController:stack0.sectionControllers[2]];
[[mockScrollDelegate expect] listAdapter:self.adapter didScrollSectionController:stack1.sectionControllers[0]];
[[mockScrollDelegate expect] listAdapter:self.adapter didScrollSectionController:stack1.sectionControllers[1]];
[self.adapter scrollViewDidScroll:self.collectionView];
[mockScrollDelegate verify];
}
- (void)test_whenForwardingWillBeginDraggingEvent_thatChildSectionControllersReceiveEvent {
[self setupWithObjects:@[
[[IGTestObject alloc] initWithKey:@0 value:@[@1, @2, @3]],
[[IGTestObject alloc] initWithKey:@2 value:@[@1, @1]]
]];
id mockScrollDelegate = [OCMockObject mockForProtocol:@protocol(IGListScrollDelegate)];
IGListStackedSectionController *stack0 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
IGListStackedSectionController *stack1 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];
[stack0.sectionControllers[0] setScrollDelegate:mockScrollDelegate];
[stack0.sectionControllers[1] setScrollDelegate:mockScrollDelegate];
[stack0.sectionControllers[2] setScrollDelegate:mockScrollDelegate];
[stack1.sectionControllers[0] setScrollDelegate:mockScrollDelegate];
[stack1.sectionControllers[1] setScrollDelegate:mockScrollDelegate];
[[mockScrollDelegate expect] listAdapter:self.adapter willBeginDraggingSectionController:stack0.sectionControllers[0]];
[[mockScrollDelegate expect] listAdapter:self.adapter willBeginDraggingSectionController:stack0.sectionControllers[1]];
[[mockScrollDelegate expect] listAdapter:self.adapter willBeginDraggingSectionController:stack0.sectionControllers[2]];
[[mockScrollDelegate expect] listAdapter:self.adapter willBeginDraggingSectionController:stack1.sectionControllers[0]];
[[mockScrollDelegate expect] listAdapter:self.adapter willBeginDraggingSectionController:stack1.sectionControllers[1]];
[self.adapter scrollViewWillBeginDragging:self.collectionView];
[mockScrollDelegate verify];
}
- (void)test_whenForwardingDidEndDraggingEvent_thatChildSectionControllersReceiveEvent {
[self setupWithObjects:@[
[[IGTestObject alloc] initWithKey:@0 value:@[@1, @2, @3]],
[[IGTestObject alloc] initWithKey:@2 value:@[@1, @1]]
]];
id mockScrollDelegate = [OCMockObject mockForProtocol:@protocol(IGListScrollDelegate)];
IGListStackedSectionController *stack0 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]];
IGListStackedSectionController *stack1 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];
[stack0.sectionControllers[0] setScrollDelegate:mockScrollDelegate];
[stack0.sectionControllers[1] setScrollDelegate:mockScrollDelegate];
[stack0.sectionControllers[2] setScrollDelegate:mockScrollDelegate];
[stack1.sectionControllers[0] setScrollDelegate:mockScrollDelegate];
[stack1.sectionControllers[1] setScrollDelegate:mockScrollDelegate];
[[mockScrollDelegate expect] listAdapter:self.adapter didEndDraggingSectionController:stack0.sectionControllers[0] willDecelerate:NO];
[[mockScrollDelegate expect] listAdapter:self.adapter didEndDraggingSectionController:stack0.sectionControllers[1] willDecelerate:NO];
[[mockScrollDelegate expect] listAdapter:self.adapter didEndDraggingSectionController:stack0.sectionControllers[2] willDecelerate:NO];
[[mockScrollDelegate expect] listAdapter:self.adapter didEndDraggingSectionController:stack1.sectionControllers[0] willDecelerate:NO];
[[mockScrollDelegate expect] listAdapter:self.adapter didEndDraggingSectionController:stack1.sectionControllers[1] willDecelerate:NO];
[self.adapter scrollViewDidEndDragging:self.collectionView willDecelerate:NO];
[mockScrollDelegate verify];
}
- (void)test_whenUsingSupplementary_withCode_thatSupplementaryViewExists {
// updater that uses reloadData so we can rebuild all views/sizes
IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListReloadDataUpdater new] viewController:nil workingRangeSize:0];
self.dataSource.objects = @[
[[IGTestObject alloc] initWithKey:@0 value:@[@1, @2, @3]],
[[IGTestObject alloc] initWithKey:@2 value:@[@1, @1]]
];
adapter.collectionView = self.collectionView;
adapter.dataSource = self.dataSource;
[self.collectionView layoutIfNeeded];
IGListStackedSectionController *stack = [adapter sectionControllerForObject:self.dataSource.objects[1]];
IGListTestSection *section = stack.sectionControllers.lastObject;
IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new];
// the stack acts as the collection context. manually assign it.
supplementarySource.collectionContext = stack;
// however the actual section controller the supplementary serves is a child of the stack
supplementarySource.sectionController = section;
supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionFooter];
section.supplementaryViewSource = supplementarySource;
[adapter performUpdatesAnimated:NO completion:nil];
XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter
atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]);
XCTAssertNotNil(supplementarySource);
}
- (void)test_whenUsingSupplementary_withNib_thatSupplementaryViewExists {
// updater that uses reloadData so we can rebuild all views/sizes
IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListReloadDataUpdater new] viewController:nil workingRangeSize:0];
self.dataSource.objects = @[
[[IGTestObject alloc] initWithKey:@0 value:@[@1, @2, @3]],
[[IGTestObject alloc] initWithKey:@2 value:@[@1, @1]]
];
adapter.collectionView = self.collectionView;
adapter.dataSource = self.dataSource;
[self.collectionView layoutIfNeeded];
IGListStackedSectionController *stack = [adapter sectionControllerForObject:self.dataSource.objects[1]];
IGListTestSection *section = stack.sectionControllers.lastObject;
IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new];
// the stack acts as the collection context. manually assign it.
supplementarySource.collectionContext = stack;
// however the actual section controller the supplementary serves is a child of the stack
supplementarySource.sectionController = section;
supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionFooter];
supplementarySource.dequeueFromNib = YES;
section.supplementaryViewSource = supplementarySource;
[adapter performUpdatesAnimated:NO completion:nil];
XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter
atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]);
XCTAssertNotNil(supplementarySource);
}
- (void)test_whenUsingSupplementary_withStoryboard_thatSupplementaryViewExists {
// updater that uses reloadData so we can rebuild all views/sizes
IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListReloadDataUpdater new] viewController:nil workingRangeSize:0];
self.dataSource.objects = @[
[[IGTestObject alloc] initWithKey:@0 value:@[@1, @2, @3]],
[[IGTestObject alloc] initWithKey:@2 value:@[@1, @1]]
];
adapter.collectionView = self.collectionView;
adapter.dataSource = self.dataSource;
[self.collectionView layoutIfNeeded];
IGListStackedSectionController *stack = [adapter sectionControllerForObject:self.dataSource.objects[1]];
IGListTestSection *section = stack.sectionControllers.lastObject;
IGTestStoryboardSupplementarySource *supplementarySource = [IGTestStoryboardSupplementarySource new];
// the stack acts as the collection context. manually assign it.
supplementarySource.collectionContext = stack;
// however the actual section controller the supplementary serves is a child of the stack
supplementarySource.sectionController = section;
// the "section header" property of the parent collection view must be checked
supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionHeader];
section.supplementaryViewSource = supplementarySource;
[adapter performUpdatesAnimated:NO completion:nil];
XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader
atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]);
XCTAssertNotNil(supplementarySource);
}
- (void)test_whenScrollingFromChildSectionController_thatScrollsToCorrectPosition {
// pad with enough items that we can freely scroll to the middle without accounting for content size
[self setupWithObjects:@[
[[IGTestObject alloc] initWithKey:@0 value:@[@4, @5, @6]],
[[IGTestObject alloc] initWithKey:@1 value:@[@1, @2, @3]],
[[IGTestObject alloc] initWithKey:@2 value:@[@4, @5, @6]]
]];
IGListStackedSectionController *stack = [self.adapter sectionControllerForObject:self.dataSource.objects[1]];
IGListTestSection *section = stack.sectionControllers[1];
[section.collectionContext scrollToSectionController:section atIndex:1 scrollPosition:UICollectionViewScrollPositionTop animated:NO];
// IGListTestSection cells are 100x10
XCTAssertEqual(self.collectionView.contentOffset.x, 0);
XCTAssertEqual(self.collectionView.contentOffset.y, 170);
}
@end

View file

@ -11,6 +11,7 @@
#import <IGListKit/IGListStackedSectionController.h>
#import "IGTestCell.h"
#import "IGListTestSection.h"
@implementation IGTestStackedDataSource
@ -21,9 +22,35 @@
- (IGListSectionController <IGListSectionType> *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object {
NSMutableArray *controllers = [[NSMutableArray alloc] init];
for (NSNumber *num in [(IGTestObject *)object value]) {
IGListTestSection *controller = [[IGListTestSection alloc] init];
controller.items = [num integerValue];
for (id value in [(IGTestObject *)object value]) {
id controller;
// use a standard IGListTestSection
if ([value isKindOfClass:[NSNumber class]]) {
IGListTestSection *section = [[IGListTestSection alloc] init];
section.items = [value integerValue];
controller = section;
} else if ([value isKindOfClass:[NSString class]]) {
void (^configureBlock)(id, __kindof UICollectionViewCell *) = ^(id obj, IGTestCell *cell) {
// capturing the value in block scope so we use the CHILD OBJECT of the stack
// otherwise the block uses the IGTestObject in the block param
cell.label.text = value;
};
CGSize (^sizeBlock)(id, id<IGListCollectionContext>) = ^CGSize(IGTestObject *item, id<IGListCollectionContext> collectionContext) {
return CGSizeMake([collectionContext containerSize].width, 44);
};
// use either nibs or storyboards with NSString depending on the string value
if ([value isEqualToString:@"nib"]) {
controller = [[IGListSingleSectionController alloc] initWithNibName:@"IGTestNibCell"
bundle:[NSBundle bundleForClass:self.class]
configureBlock:configureBlock
sizeBlock:sizeBlock];
} else {
controller = [[IGListSingleSectionController alloc] initWithStoryboardCellIdentifier:@"IGTestStoryboardCell"
configureBlock:configureBlock
sizeBlock:sizeBlock];
}
}
[controllers addObject:controller];
}
return [[IGListStackedSectionController alloc] initWithSectionControllers:controllers];