mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00

git-subtree-dir: submodules/AsyncDisplayKit git-subtree-mainline: d06f423e0ed3df1fed9bd10d79ee312a9179b632 git-subtree-split: 02bedc12816e251ad71777f9d2578329b6d2bef6
1174 lines
52 KiB
Plaintext
1174 lines
52 KiB
Plaintext
//
|
|
// ASCollectionViewTests.mm
|
|
// Texture
|
|
//
|
|
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
|
|
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
|
|
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
|
|
#import <XCTest/XCTest.h>
|
|
#import <AsyncDisplayKit/AsyncDisplayKit.h>
|
|
#import <AsyncDisplayKit/ASCollectionViewFlowLayoutInspector.h>
|
|
#import <AsyncDisplayKit/ASDataController.h>
|
|
#import <AsyncDisplayKit/ASSectionContext.h>
|
|
#import <vector>
|
|
#import <OCMock/OCMock.h>
|
|
#import <AsyncDisplayKit/ASCollectionView+Undeprecated.h>
|
|
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
|
|
#import "ASDisplayNodeTestsHelper.h"
|
|
|
|
@interface ASTextCellNodeWithSetSelectedCounter : ASTextCellNode
|
|
|
|
@property (nonatomic) NSUInteger setSelectedCounter;
|
|
@property (nonatomic) NSUInteger applyLayoutAttributesCount;
|
|
|
|
@end
|
|
|
|
@implementation ASTextCellNodeWithSetSelectedCounter
|
|
|
|
- (void)setSelected:(BOOL)selected
|
|
{
|
|
[super setSelected:selected];
|
|
_setSelectedCounter++;
|
|
}
|
|
|
|
- (void)applyLayoutAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
|
|
{
|
|
_applyLayoutAttributesCount++;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface ASTestSectionContext : NSObject <ASSectionContext>
|
|
|
|
@property (nonatomic) NSInteger sectionIndex;
|
|
@property (nonatomic) NSInteger sectionGeneration;
|
|
|
|
@end
|
|
|
|
@implementation ASTestSectionContext
|
|
|
|
@synthesize sectionName = _sectionName, collectionView = _collectionView;
|
|
|
|
@end
|
|
|
|
@interface ASCollectionViewTestDelegate : NSObject <ASCollectionDataSource, ASCollectionDelegate, UICollectionViewDelegateFlowLayout>
|
|
|
|
@property (nonatomic) NSInteger sectionGeneration;
|
|
@property (nonatomic) void(^willBeginBatchFetch)(ASBatchContext *);
|
|
|
|
@end
|
|
|
|
@implementation ASCollectionViewTestDelegate {
|
|
@package
|
|
std::vector<NSInteger> _itemCounts;
|
|
}
|
|
|
|
- (id)initWithNumberOfSections:(NSInteger)numberOfSections numberOfItemsInSection:(NSInteger)numberOfItemsInSection {
|
|
if (self = [super init]) {
|
|
for (NSInteger i = 0; i < numberOfSections; i++) {
|
|
_itemCounts.push_back(numberOfItemsInSection);
|
|
}
|
|
_sectionGeneration = 1;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
ASTextCellNodeWithSetSelectedCounter *textCellNode = [ASTextCellNodeWithSetSelectedCounter new];
|
|
textCellNode.text = indexPath.description;
|
|
|
|
return textCellNode;
|
|
}
|
|
|
|
|
|
- (ASCellNodeBlock)collectionView:(ASCollectionView *)collectionView nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
return ^{
|
|
ASTextCellNodeWithSetSelectedCounter *textCellNode = [ASTextCellNodeWithSetSelectedCounter new];
|
|
textCellNode.text = indexPath.description;
|
|
return textCellNode;
|
|
};
|
|
}
|
|
|
|
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
|
|
return _itemCounts.size();
|
|
}
|
|
|
|
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
|
return _itemCounts[section];
|
|
}
|
|
|
|
- (id<ASSectionContext>)collectionNode:(ASCollectionNode *)collectionNode contextForSection:(NSInteger)section
|
|
{
|
|
ASTestSectionContext *context = [[ASTestSectionContext alloc] init];
|
|
context.sectionGeneration = _sectionGeneration;
|
|
context.sectionIndex = section;
|
|
return context;
|
|
}
|
|
|
|
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section
|
|
{
|
|
return CGSizeMake(100, 100);
|
|
}
|
|
|
|
- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
return [[ASTextCellNodeWithSetSelectedCounter alloc] init];
|
|
}
|
|
|
|
- (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context
|
|
{
|
|
if (_willBeginBatchFetch != nil) {
|
|
_willBeginBatchFetch(context);
|
|
} else {
|
|
[context cancelBatchFetching];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@interface ASCollectionViewTestController: UIViewController
|
|
|
|
@property (nonatomic) ASCollectionViewTestDelegate *asyncDelegate;
|
|
@property (nonatomic) ASCollectionView *collectionView;
|
|
@property (nonatomic) ASCollectionNode *collectionNode;
|
|
|
|
@end
|
|
|
|
@implementation ASCollectionViewTestController
|
|
|
|
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
|
|
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
|
if (self) {
|
|
// Populate these immediately so that they're not unexpectedly nil during tests.
|
|
self.asyncDelegate = [[ASCollectionViewTestDelegate alloc] initWithNumberOfSections:10 numberOfItemsInSection:10];
|
|
id realLayout = [UICollectionViewFlowLayout new];
|
|
id mockLayout = [OCMockObject partialMockForObject:realLayout];
|
|
self.collectionNode = [[ASCollectionNode alloc] initWithFrame:self.view.bounds collectionViewLayout:mockLayout];
|
|
self.collectionView = self.collectionNode.view;
|
|
self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
self.collectionNode.dataSource = self.asyncDelegate;
|
|
self.collectionNode.delegate = self.asyncDelegate;
|
|
|
|
[self.collectionNode registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader];
|
|
[self.view addSubview:self.collectionView];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface ASCollectionView (InternalTesting)
|
|
|
|
- (NSArray<NSString *> *)dataController:(ASDataController *)dataController supplementaryNodeKindsInSections:(NSIndexSet *)sections;
|
|
|
|
@end
|
|
|
|
@interface ASCollectionViewTests : XCTestCase
|
|
|
|
@end
|
|
|
|
@implementation ASCollectionViewTests
|
|
|
|
- (void)tearDown
|
|
{
|
|
// We can't prevent the system from retaining windows, but we can at least clear them out to avoid
|
|
// pollution between test cases.
|
|
for (UIWindow *window in [UIApplication sharedApplication].windows) {
|
|
for (UIView *subview in window.subviews) {
|
|
[subview removeFromSuperview];
|
|
}
|
|
}
|
|
[super tearDown];
|
|
}
|
|
|
|
- (void)testDataSourceImplementsNecessaryMethods
|
|
{
|
|
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
|
ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
|
|
|
id dataSource = [NSObject new];
|
|
XCTAssertThrows((collectionView.asyncDataSource = dataSource));
|
|
|
|
dataSource = [OCMockObject niceMockForProtocol:@protocol(ASCollectionDataSource)];
|
|
XCTAssertNoThrow((collectionView.asyncDataSource = dataSource));
|
|
}
|
|
|
|
- (void)testThatItSetsALayoutInspectorForFlowLayouts
|
|
{
|
|
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
|
ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
|
XCTAssert(collectionView.layoutInspector != nil, @"should automatically set a layout delegate for flow layouts");
|
|
XCTAssert([collectionView.layoutInspector isKindOfClass:[ASCollectionViewFlowLayoutInspector class]], @"should have a flow layout inspector by default");
|
|
}
|
|
|
|
- (void)testThatADefaultLayoutInspectorIsProvidedForCustomLayouts
|
|
{
|
|
UICollectionViewLayout *layout = [[UICollectionViewLayout alloc] init];
|
|
ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
|
XCTAssert(collectionView.layoutInspector != nil, @"should automatically set a layout delegate for flow layouts");
|
|
XCTAssert([collectionView.layoutInspector isKindOfClass:[ASCollectionViewLayoutInspector class]], @"should have a default layout inspector by default");
|
|
}
|
|
|
|
- (void)testThatRegisteringASupplementaryNodeStoresItForIntrospection
|
|
{
|
|
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
|
ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
|
[collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader];
|
|
XCTAssertEqualObjects([collectionView dataController:nil supplementaryNodeKindsInSections:[NSIndexSet indexSetWithIndex:0]], @[UICollectionElementKindSectionHeader]);
|
|
}
|
|
|
|
- (void)testReloadIfNeeded
|
|
{
|
|
__block ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
__block ASCollectionViewTestDelegate *del = testController.asyncDelegate;
|
|
__block ASCollectionNode *cn = testController.collectionNode;
|
|
|
|
void (^reset)() = ^void() {
|
|
testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
del = testController.asyncDelegate;
|
|
cn = testController.collectionNode;
|
|
};
|
|
|
|
// Check if the number of sections matches the data source
|
|
XCTAssertEqual(cn.numberOfSections, del->_itemCounts.size(), @"Section count doesn't match the data source");
|
|
|
|
// Reset everything and then check if numberOfItemsInSection matches the data source
|
|
reset();
|
|
XCTAssertEqual([cn numberOfItemsInSection:0], del->_itemCounts[0], @"Number of items in Section doesn't match the data source");
|
|
|
|
// Reset and check if we can get the node corresponding to a specific indexPath
|
|
reset();
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
|
|
ASTextCellNodeWithSetSelectedCounter *node = (ASTextCellNodeWithSetSelectedCounter*)[cn nodeForItemAtIndexPath:indexPath];
|
|
XCTAssertTrue([node.text isEqualToString:indexPath.description], @"Node's text should match the initial text it was created with");
|
|
}
|
|
|
|
- (void)testSelection
|
|
{
|
|
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
|
[window setRootViewController:testController];
|
|
[window makeKeyAndVisible];
|
|
|
|
[testController.collectionNode reloadData];
|
|
[testController.collectionNode waitUntilAllUpdatesAreProcessed];
|
|
[testController.collectionView layoutIfNeeded];
|
|
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
|
|
ASCellNode *node = [testController.collectionView nodeForItemAtIndexPath:indexPath];
|
|
|
|
NSInteger setSelectedCount = 0;
|
|
// selecting node should select cell
|
|
node.selected = YES;
|
|
++setSelectedCount;
|
|
XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath], @"Selecting node should update cell selection.");
|
|
|
|
// deselecting node should deselect cell
|
|
node.selected = NO;
|
|
++setSelectedCount;
|
|
XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] isEqualToArray:@[]], @"Deselecting node should update cell selection.");
|
|
|
|
// selecting cell via collectionNode should select node
|
|
++setSelectedCount;
|
|
[testController.collectionNode selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
|
|
XCTAssertTrue(node.isSelected == YES, @"Selecting cell should update node selection.");
|
|
|
|
// deselecting cell via collectionNode should deselect node
|
|
++setSelectedCount;
|
|
[testController.collectionNode deselectItemAtIndexPath:indexPath animated:NO];
|
|
XCTAssertTrue(node.isSelected == NO, @"Deselecting cell should update node selection.");
|
|
|
|
// select the cell again, scroll down and back up, and check that the state persisted
|
|
[testController.collectionNode selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
|
|
++setSelectedCount;
|
|
XCTAssertTrue(node.isSelected == YES, @"Selecting cell should update node selection.");
|
|
|
|
testController.collectionNode.allowsMultipleSelection = YES;
|
|
|
|
NSIndexPath *indexPath2 = [NSIndexPath indexPathForItem:1 inSection:0];
|
|
ASCellNode *node2 = [testController.collectionView nodeForItemAtIndexPath:indexPath2];
|
|
|
|
// selecting cell via collectionNode should select node
|
|
[testController.collectionNode selectItemAtIndexPath:indexPath2 animated:NO scrollPosition:UICollectionViewScrollPositionNone];
|
|
XCTAssertTrue(node2.isSelected == YES, @"Selecting cell should update node selection.");
|
|
|
|
XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath] &&
|
|
[[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath2],
|
|
@"Selecting multiple cells should result in those cells being in the array of selectedItems.");
|
|
|
|
// deselecting node should deselect cell
|
|
node.selected = NO;
|
|
++setSelectedCount;
|
|
XCTAssertTrue(![[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath] &&
|
|
[[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath2], @"Deselecting node should update array of selectedItems.");
|
|
|
|
node.selected = YES;
|
|
++setSelectedCount;
|
|
XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath], @"Selecting node should update cell selection.");
|
|
|
|
node2.selected = NO;
|
|
XCTAssertTrue([[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath] &&
|
|
![[testController.collectionView indexPathsForSelectedItems] containsObject:indexPath2], @"Deselecting node should update array of selectedItems.");
|
|
|
|
// reload cell (-prepareForReuse is called) & check that selected state is preserved
|
|
[testController.collectionView setContentOffset:CGPointMake(0,testController.collectionView.bounds.size.height)];
|
|
[testController.collectionView layoutIfNeeded];
|
|
[testController.collectionView setContentOffset:CGPointMake(0,0)];
|
|
[testController.collectionView layoutIfNeeded];
|
|
XCTAssertTrue(node.isSelected == YES, @"Reloaded cell should preserve state.");
|
|
|
|
// deselecting cell should deselect node
|
|
UICollectionViewCell *cell = [testController.collectionView cellForItemAtIndexPath:indexPath];
|
|
cell.selected = NO;
|
|
XCTAssertTrue(node.isSelected == NO, @"Deselecting cell should update node selection.");
|
|
|
|
// check setSelected not called extra times
|
|
XCTAssertTrue([(ASTextCellNodeWithSetSelectedCounter *)node setSelectedCounter] == (setSelectedCount + 1), @"setSelected: should not be called on node multiple times.");
|
|
}
|
|
|
|
- (void)testTuningParametersWithExplicitRangeMode
|
|
{
|
|
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
|
ASCollectionNode *collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout];
|
|
|
|
ASRangeTuningParameters minimumRenderParams = { .leadingBufferScreenfuls = 0.1, .trailingBufferScreenfuls = 0.1 };
|
|
ASRangeTuningParameters minimumPreloadParams = { .leadingBufferScreenfuls = 0.1, .trailingBufferScreenfuls = 0.1 };
|
|
ASRangeTuningParameters fullRenderParams = { .leadingBufferScreenfuls = 0.5, .trailingBufferScreenfuls = 0.5 };
|
|
ASRangeTuningParameters fullPreloadParams = { .leadingBufferScreenfuls = 1, .trailingBufferScreenfuls = 0.5 };
|
|
|
|
[collectionNode setTuningParameters:minimumRenderParams forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypeDisplay];
|
|
[collectionNode setTuningParameters:minimumPreloadParams forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload];
|
|
[collectionNode setTuningParameters:fullRenderParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypeDisplay];
|
|
[collectionNode setTuningParameters:fullPreloadParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypePreload];
|
|
|
|
XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(minimumRenderParams,
|
|
[collectionNode tuningParametersForRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypeDisplay]));
|
|
XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(minimumPreloadParams,
|
|
[collectionNode tuningParametersForRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload]));
|
|
XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(fullRenderParams,
|
|
[collectionNode tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypeDisplay]));
|
|
XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(fullPreloadParams,
|
|
[collectionNode tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypePreload]));
|
|
}
|
|
|
|
- (void)testTuningParameters
|
|
{
|
|
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
|
ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
|
|
|
ASRangeTuningParameters renderParams = { .leadingBufferScreenfuls = 1.2, .trailingBufferScreenfuls = 3.2 };
|
|
ASRangeTuningParameters preloadParams = { .leadingBufferScreenfuls = 4.3, .trailingBufferScreenfuls = 2.3 };
|
|
|
|
[collectionView setTuningParameters:renderParams forRangeType:ASLayoutRangeTypeDisplay];
|
|
[collectionView setTuningParameters:preloadParams forRangeType:ASLayoutRangeTypePreload];
|
|
|
|
XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(renderParams, [collectionView tuningParametersForRangeType:ASLayoutRangeTypeDisplay]));
|
|
XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(preloadParams, [collectionView tuningParametersForRangeType:ASLayoutRangeTypePreload]));
|
|
}
|
|
|
|
// Informations to test: https://github.com/TextureGroup/Texture/issues/1094
|
|
- (void)testThatCollectionNodeCanHandleNilRangeController
|
|
{
|
|
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
|
ASCollectionNode *collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout];
|
|
[collectionNode recursivelySetInterfaceState:ASInterfaceStateDisplay];
|
|
[collectionNode setHierarchyState:ASHierarchyStateRangeManaged];
|
|
[collectionNode recursivelySetInterfaceState:ASInterfaceStateNone];
|
|
ASCATransactionQueueWait(nil);
|
|
}
|
|
|
|
/**
|
|
* This may seem silly, but we had issues where the runtime sometimes wouldn't correctly report
|
|
* conformances declared on categories.
|
|
*/
|
|
- (void)testThatCollectionNodeConformsToExpectedProtocols
|
|
{
|
|
ASCollectionNode *node = [[ASCollectionNode alloc] initWithFrame:CGRectZero collectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
|
|
XCTAssert([node conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)]);
|
|
}
|
|
|
|
#pragma mark - Update Validations
|
|
|
|
#define updateValidationTestPrologue \
|
|
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];\
|
|
__unused ASCollectionViewTestDelegate *del = testController.asyncDelegate;\
|
|
__unused ASCollectionView *cv = testController.collectionView;\
|
|
ASCollectionNode *cn = testController.collectionNode;\
|
|
UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];\
|
|
[window makeKeyAndVisible]; \
|
|
window.rootViewController = testController;\
|
|
\
|
|
[cn reloadData];\
|
|
[cn waitUntilAllUpdatesAreProcessed]; \
|
|
[testController.collectionView layoutIfNeeded];
|
|
|
|
- (void)testThatSubmittingAValidInsertDoesNotThrowAnException
|
|
{
|
|
updateValidationTestPrologue
|
|
NSInteger sectionCount = del->_itemCounts.size();
|
|
|
|
del->_itemCounts[sectionCount - 1]++;
|
|
XCTAssertNoThrow([cv insertItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount - 1] ]]);
|
|
}
|
|
|
|
- (void)testThatSubmittingAValidReloadDoesNotThrowAnException
|
|
{
|
|
updateValidationTestPrologue
|
|
NSInteger sectionCount = del->_itemCounts.size();
|
|
|
|
XCTAssertNoThrow([cv reloadItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount - 1] ]]);
|
|
}
|
|
|
|
- (void)testThatSubmittingAnInvalidInsertThrowsAnException
|
|
{
|
|
updateValidationTestPrologue
|
|
NSInteger sectionCount = del->_itemCounts.size();
|
|
|
|
XCTAssertThrows([cv insertItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount + 1] ]]);
|
|
}
|
|
|
|
- (void)testThatSubmittingAnInvalidDeleteThrowsAnException
|
|
{
|
|
updateValidationTestPrologue
|
|
NSInteger sectionCount = del->_itemCounts.size();
|
|
|
|
XCTAssertThrows([cv deleteItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount + 1] ]]);
|
|
}
|
|
|
|
- (void)testThatDeletingAndReloadingTheSameItemThrowsAnException
|
|
{
|
|
updateValidationTestPrologue
|
|
|
|
XCTAssertThrows([cv performBatchUpdates:^{
|
|
NSArray *indexPaths = @[ [NSIndexPath indexPathForItem:0 inSection:0] ];
|
|
[cv deleteItemsAtIndexPaths:indexPaths];
|
|
[cv reloadItemsAtIndexPaths:indexPaths];
|
|
} completion:nil]);
|
|
}
|
|
|
|
- (void)testThatHavingAnIncorrectSectionCountThrowsAnException
|
|
{
|
|
updateValidationTestPrologue
|
|
|
|
XCTAssertThrows([cv deleteSections:[NSIndexSet indexSetWithIndex:0]]);
|
|
}
|
|
|
|
- (void)testThatHavingAnIncorrectItemCountThrowsAnException
|
|
{
|
|
updateValidationTestPrologue
|
|
|
|
XCTAssertThrows([cv deleteItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:0] ]]);
|
|
}
|
|
|
|
- (void)testThatHavingAnIncorrectItemCountWithNoUpdatesThrowsAnException
|
|
{
|
|
updateValidationTestPrologue
|
|
|
|
XCTAssertThrows([cv performBatchUpdates:^{
|
|
del->_itemCounts[0]++;
|
|
} completion:nil]);
|
|
}
|
|
|
|
- (void)testThatInsertingAnInvalidSectionThrowsAnException
|
|
{
|
|
updateValidationTestPrologue
|
|
NSInteger sectionCount = del->_itemCounts.size();
|
|
|
|
del->_itemCounts.push_back(10);
|
|
XCTAssertThrows([cv performBatchUpdates:^{
|
|
[cv insertSections:[NSIndexSet indexSetWithIndex:sectionCount + 1]];
|
|
} completion:nil]);
|
|
}
|
|
|
|
- (void)testThatDeletingAndReloadingASectionThrowsAnException
|
|
{
|
|
updateValidationTestPrologue
|
|
NSInteger sectionCount = del->_itemCounts.size();
|
|
|
|
del->_itemCounts.pop_back();
|
|
XCTAssertThrows([cv performBatchUpdates:^{
|
|
NSIndexSet *sections = [NSIndexSet indexSetWithIndex:sectionCount - 1];
|
|
[cv reloadSections:sections];
|
|
[cv deleteSections:sections];
|
|
} completion:nil]);
|
|
}
|
|
|
|
- (void)testCellNodeLayoutAttributes
|
|
{
|
|
updateValidationTestPrologue
|
|
NSSet *nodeBatch1 = [NSSet setWithArray:[cn visibleNodes]];
|
|
XCTAssertGreaterThan(nodeBatch1.count, 0);
|
|
|
|
NSArray<UICollectionViewLayoutAttributes *> *visibleLayoutAttributesBatch1 = [cv.collectionViewLayout layoutAttributesForElementsInRect:cv.bounds];
|
|
XCTAssertGreaterThan(visibleLayoutAttributesBatch1.count, 0);
|
|
|
|
// Expect all visible nodes get 1 applyLayoutAttributes and have a non-nil value.
|
|
for (ASTextCellNodeWithSetSelectedCounter *node in nodeBatch1) {
|
|
XCTAssertEqual(node.applyLayoutAttributesCount, 1, @"Expected applyLayoutAttributes to be called exactly once for visible nodes.");
|
|
XCTAssertNotNil(node.layoutAttributes, @"Expected layoutAttributes to be non-nil for visible cell node.");
|
|
}
|
|
|
|
for (UICollectionViewLayoutAttributes *layoutAttributes in visibleLayoutAttributesBatch1) {
|
|
if (layoutAttributes.representedElementCategory != UICollectionElementCategorySupplementaryView) {
|
|
continue;
|
|
}
|
|
ASTextCellNodeWithSetSelectedCounter *node = (ASTextCellNodeWithSetSelectedCounter *)[cv supplementaryNodeForElementKind:layoutAttributes.representedElementKind atIndexPath:layoutAttributes.indexPath];
|
|
XCTAssertEqual(node.applyLayoutAttributesCount, 1, @"Expected applyLayoutAttributes to be called exactly once for visible supplementary nodes.");
|
|
XCTAssertNotNil(node.layoutAttributes, @"Expected layoutAttributes to be non-nil for visible supplementary node.");
|
|
}
|
|
|
|
// Scroll to next batch of items.
|
|
NSIndexPath *nextIP = [NSIndexPath indexPathForItem:nodeBatch1.count inSection:0];
|
|
[cv scrollToItemAtIndexPath:nextIP atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
|
|
[cv layoutIfNeeded];
|
|
|
|
// Ensure we scrolled far enough that all the old ones are offscreen.
|
|
NSSet *nodeBatch2 = [NSSet setWithArray:[cn visibleNodes]];
|
|
XCTAssertFalse([nodeBatch1 intersectsSet:nodeBatch2], @"Expected to scroll far away enough that all nodes are replaced.");
|
|
|
|
// Now the nodes are no longer visible, expect their layout attributes are nil but not another applyLayoutAttributes call.
|
|
for (ASTextCellNodeWithSetSelectedCounter *node in nodeBatch1) {
|
|
XCTAssertEqual(node.applyLayoutAttributesCount, 1, @"Expected applyLayoutAttributes to be called exactly once for visible nodes, even after node is removed.");
|
|
XCTAssertNil(node.layoutAttributes, @"Expected layoutAttributes to be nil for removed cell node.");
|
|
}
|
|
|
|
for (UICollectionViewLayoutAttributes *layoutAttributes in visibleLayoutAttributesBatch1) {
|
|
if (layoutAttributes.representedElementCategory != UICollectionElementCategorySupplementaryView) {
|
|
continue;
|
|
}
|
|
ASTextCellNodeWithSetSelectedCounter *node = (ASTextCellNodeWithSetSelectedCounter *)[cv supplementaryNodeForElementKind:layoutAttributes.representedElementKind atIndexPath:layoutAttributes.indexPath];
|
|
XCTAssertEqual(node.applyLayoutAttributesCount, 1, @"Expected applyLayoutAttributes to be called exactly once for visible supplementary nodes, even after node is removed.");
|
|
XCTAssertNil(node.layoutAttributes, @"Expected layoutAttributes to be nil for removed supplementary node.");
|
|
}
|
|
}
|
|
|
|
- (void)testCellNodeIndexPathConsistency
|
|
{
|
|
updateValidationTestPrologue
|
|
|
|
// Test with a visible cell
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:2 inSection:0];
|
|
ASCellNode *cell = [cn nodeForItemAtIndexPath:indexPath];
|
|
|
|
// Check if cell's indexPath corresponds to the indexPath being tested
|
|
XCTAssertTrue(cell.indexPath.section == indexPath.section && cell.indexPath.item == indexPath.item, @"Expected the cell's indexPath to be the same as the indexPath being tested.");
|
|
|
|
// Remove an item prior to the cell's indexPath from the same section and check for indexPath consistency
|
|
--del->_itemCounts[indexPath.section];
|
|
[cn deleteItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:indexPath.section]]];
|
|
XCTAssertTrue(cell.indexPath.section == indexPath.section && cell.indexPath.item == (indexPath.item - 1), @"Expected the cell's indexPath to be updated once a cell with a lower index is deleted.");
|
|
|
|
// Remove the section that includes the indexPath and check if the cell's indexPath is now nil
|
|
del->_itemCounts.erase(del->_itemCounts.begin());
|
|
[cn deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section]];
|
|
XCTAssertNil(cell.indexPath, @"Expected the cell's indexPath to be nil once the section that contains the node is deleted.");
|
|
|
|
// Run the same tests but with a non-displayed cell
|
|
indexPath = [NSIndexPath indexPathForItem:2 inSection:(del->_itemCounts.size() - 1)];
|
|
cell = [cn nodeForItemAtIndexPath:indexPath];
|
|
|
|
// Check if cell's indexPath corresponds to the indexPath being tested
|
|
XCTAssertTrue(cell.indexPath.section == indexPath.section && cell.indexPath.item == indexPath.item, @"Expected the cell's indexPath to be the same as the indexPath in question.");
|
|
|
|
// Remove an item prior to the cell's indexPath from the same section and check for indexPath consistency
|
|
--del->_itemCounts[indexPath.section];
|
|
[cn deleteItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:indexPath.section]]];
|
|
XCTAssertTrue(cell.indexPath.section == indexPath.section && cell.indexPath.item == (indexPath.item - 1), @"Expected the cell's indexPath to be updated once a cell with a lower index is deleted.");
|
|
|
|
// Remove the section that includes the indexPath and check if the cell's indexPath is now nil
|
|
del->_itemCounts.pop_back();
|
|
[cn deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section]];
|
|
XCTAssertNil(cell.indexPath, @"Expected the cell's indexPath to be nil once the section that contains the node is deleted.");
|
|
}
|
|
|
|
/**
|
|
* https://github.com/facebook/AsyncDisplayKit/issues/2011
|
|
*
|
|
* If this ever becomes a pain to maintain, drop it. The underlying issue is tested by testThatLayerBackedSubnodesAreMarkedInvisibleBeforeDeallocWhenSupernodesViewIsRemovedFromHierarchyWhileBeingRetained
|
|
*/
|
|
- (void)testThatDisappearingSupplementariesWithLayerBackedNodesDontFailAssert
|
|
{
|
|
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
|
UICollectionViewLayout *layout = [[UICollectionViewFlowLayout alloc] init];
|
|
ASCollectionNode *cn = [[ASCollectionNode alloc] initWithFrame:window.bounds collectionViewLayout:layout];
|
|
ASCollectionView *cv = cn.view;
|
|
|
|
|
|
__unused NSMutableSet *keepaliveNodes = [NSMutableSet set];
|
|
id dataSource = [OCMockObject niceMockForProtocol:@protocol(ASCollectionDataSource)];
|
|
static int nodeIdx = 0;
|
|
[[[dataSource stub] andDo:^(NSInvocation *invocation) {
|
|
__autoreleasing ASCellNode *suppNode = [[ASCellNode alloc] init];
|
|
int thisNodeIdx = nodeIdx++;
|
|
suppNode.debugName = [NSString stringWithFormat:@"Cell #%d", thisNodeIdx];
|
|
[keepaliveNodes addObject:suppNode];
|
|
|
|
ASDisplayNode *layerBacked = [[ASDisplayNode alloc] init];
|
|
layerBacked.layerBacked = YES;
|
|
layerBacked.debugName = [NSString stringWithFormat:@"Subnode #%d", thisNodeIdx];
|
|
[suppNode addSubnode:layerBacked];
|
|
[invocation setReturnValue:&suppNode];
|
|
}] collectionNode:cn nodeForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:OCMOCK_ANY];
|
|
[[[dataSource stub] andReturnValue:[NSNumber numberWithInteger:1]] numberOfSectionsInCollectionView:cv];
|
|
cv.asyncDataSource = dataSource;
|
|
|
|
id delegate = [OCMockObject niceMockForProtocol:@protocol(UICollectionViewDelegateFlowLayout)];
|
|
[[[delegate stub] andReturnValue:[NSValue valueWithCGSize:CGSizeMake(100, 100)]] collectionView:cv layout:OCMOCK_ANY referenceSizeForHeaderInSection:0];
|
|
cv.asyncDelegate = delegate;
|
|
|
|
[cv registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader];
|
|
[window addSubview:cv];
|
|
|
|
[window makeKeyAndVisible];
|
|
|
|
for (NSInteger i = 0; i < 2; i++) {
|
|
// NOTE: reloadData and waitUntilAllUpdatesAreProcessed are not sufficient here!!
|
|
XCTestExpectation *done = [self expectationWithDescription:[NSString stringWithFormat:@"Reload #%td complete", i]];
|
|
[cn reloadDataWithCompletion:^{
|
|
[done fulfill];
|
|
}];
|
|
[self waitForExpectationsWithTimeout:1 handler:nil];
|
|
}
|
|
|
|
}
|
|
|
|
- (void)testThatNodeCalculatedSizesAreUpdatedBeforeFirstPrepareLayoutAfterRotation
|
|
{
|
|
updateValidationTestPrologue
|
|
id layout = cv.collectionViewLayout;
|
|
CGSize initialItemSize = [cv nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]].calculatedSize;
|
|
CGSize initialCVSize = cv.bounds.size;
|
|
|
|
// Capture the node size before first call to prepareLayout after frame change.
|
|
__block CGSize itemSizeAtFirstLayout = CGSizeZero;
|
|
__block CGSize boundsSizeAtFirstLayout = CGSizeZero;
|
|
[[[[layout expect] andDo:^(NSInvocation *) {
|
|
itemSizeAtFirstLayout = [cv nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]].calculatedSize;
|
|
boundsSizeAtFirstLayout = [cv bounds].size;
|
|
}] andForwardToRealObject] prepareLayout];
|
|
|
|
// Rotate the device
|
|
UIDeviceOrientation oldDeviceOrientation = [[UIDevice currentDevice] orientation];
|
|
[[UIDevice currentDevice] setValue:@(UIDeviceOrientationLandscapeLeft) forKey:@"orientation"];
|
|
|
|
CGSize finalItemSize = [cv nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]].calculatedSize;
|
|
CGSize finalCVSize = cv.bounds.size;
|
|
XCTAssertNotEqualObjects(NSStringFromCGSize(initialItemSize), NSStringFromCGSize(itemSizeAtFirstLayout));
|
|
XCTAssertNotEqualObjects(NSStringFromCGSize(initialCVSize), NSStringFromCGSize(boundsSizeAtFirstLayout));
|
|
XCTAssertEqualObjects(NSStringFromCGSize(itemSizeAtFirstLayout), NSStringFromCGSize(finalItemSize));
|
|
XCTAssertEqualObjects(NSStringFromCGSize(boundsSizeAtFirstLayout), NSStringFromCGSize(finalCVSize));
|
|
[layout verify];
|
|
|
|
// Teardown
|
|
[[UIDevice currentDevice] setValue:@(oldDeviceOrientation) forKey:@"orientation"];
|
|
}
|
|
|
|
/**
|
|
* See corresponding test in ASUICollectionViewTests
|
|
*
|
|
* @discussion Currently, we do not replicate UICollectionView's call order (outer, inner0, inner1, ...)
|
|
* and instead call (inner0, inner1, outer, ...). This is because we primarily provide a
|
|
* beginUpdates/endUpdatesWithCompletion: interface (like UITableView). With UICollectionView's
|
|
* performBatchUpdates:completion:, the completion block is enqueued at -beginUpdates time.
|
|
* With our tableView-like scheme, the completion block is provided at -endUpdates time
|
|
* and it is naturally enqueued at this time. It is assumed that this is an acceptable deviation,
|
|
* and that developers do not expect a particular completion order guarantee.
|
|
*/
|
|
- (void)testThatNestedBatchCompletionsAreCalledInOrder
|
|
{
|
|
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
|
|
ASCollectionView *cv = testController.collectionView;
|
|
|
|
XCTestExpectation *inner0 = [self expectationWithDescription:@"Inner completion 0 is called"];
|
|
XCTestExpectation *inner1 = [self expectationWithDescription:@"Inner completion 1 is called"];
|
|
XCTestExpectation *outer = [self expectationWithDescription:@"Outer completion is called"];
|
|
|
|
NSMutableArray<XCTestExpectation *> *completions = [NSMutableArray array];
|
|
|
|
[cv performBatchUpdates:^{
|
|
[cv performBatchUpdates:^{
|
|
|
|
} completion:^(BOOL finished) {
|
|
[completions addObject:inner0];
|
|
[inner0 fulfill];
|
|
}];
|
|
[cv performBatchUpdates:^{
|
|
|
|
} completion:^(BOOL finished) {
|
|
[completions addObject:inner1];
|
|
[inner1 fulfill];
|
|
}];
|
|
} completion:^(BOOL finished) {
|
|
[completions addObject:outer];
|
|
[outer fulfill];
|
|
}];
|
|
|
|
[self waitForExpectationsWithTimeout:5 handler:nil];
|
|
XCTAssertEqualObjects(completions, (@[ inner0, inner1, outer ]), @"Expected completion order to be correct");
|
|
}
|
|
|
|
#pragma mark - ASSectionContext tests
|
|
|
|
- (void)testThatSectionContextsAreCorrectAfterTheInitialLayout
|
|
{
|
|
updateValidationTestPrologue
|
|
NSInteger sectionCount = del->_itemCounts.size();
|
|
for (NSInteger section = 0; section < sectionCount; section++) {
|
|
ASTestSectionContext *context = (ASTestSectionContext *)[cn contextForSection:section];
|
|
XCTAssertNotNil(context);
|
|
XCTAssertEqual(context.sectionGeneration, 1);
|
|
XCTAssertEqual(context.sectionIndex, section);
|
|
}
|
|
}
|
|
|
|
- (void)testThatSectionContextsAreCorrectAfterSectionMove
|
|
{
|
|
updateValidationTestPrologue
|
|
NSInteger sectionCount = del->_itemCounts.size();
|
|
NSInteger originalSection = sectionCount - 1;
|
|
NSInteger toSection = 0;
|
|
|
|
del.sectionGeneration++;
|
|
[cv moveSection:originalSection toSection:toSection];
|
|
[cv waitUntilAllUpdatesAreCommitted];
|
|
|
|
// Only test left moving
|
|
XCTAssertTrue(toSection < originalSection);
|
|
ASTestSectionContext *movedSectionContext = (ASTestSectionContext *)[cn contextForSection:toSection];
|
|
XCTAssertNotNil(movedSectionContext);
|
|
// ASCollectionView currently splits a move operation to a pair of delete and insert ones.
|
|
// So this movedSectionContext is newly loaded and thus is second generation.
|
|
XCTAssertEqual(movedSectionContext.sectionGeneration, 2);
|
|
XCTAssertEqual(movedSectionContext.sectionIndex, toSection);
|
|
|
|
for (NSInteger section = toSection + 1; section <= originalSection && section < sectionCount; section++) {
|
|
ASTestSectionContext *context = (ASTestSectionContext *)[cn contextForSection:section];
|
|
XCTAssertNotNil(context);
|
|
XCTAssertEqual(context.sectionGeneration, 1);
|
|
// This section context was shifted to the right
|
|
XCTAssertEqual(context.sectionIndex, (section - 1));
|
|
}
|
|
}
|
|
|
|
- (void)testThatSectionContextsAreCorrectAfterReloadData
|
|
{
|
|
updateValidationTestPrologue
|
|
|
|
del.sectionGeneration++;
|
|
[cn reloadData];
|
|
[cn waitUntilAllUpdatesAreProcessed];
|
|
|
|
NSInteger sectionCount = del->_itemCounts.size();
|
|
for (NSInteger section = 0; section < sectionCount; section++) {
|
|
ASTestSectionContext *context = (ASTestSectionContext *)[cn contextForSection:section];
|
|
XCTAssertNotNil(context);
|
|
XCTAssertEqual(context.sectionGeneration, 2);
|
|
XCTAssertEqual(context.sectionIndex, section);
|
|
}
|
|
}
|
|
|
|
- (void)testThatSectionContextsAreCorrectAfterReloadASection
|
|
{
|
|
updateValidationTestPrologue
|
|
NSInteger sectionToReload = 0;
|
|
|
|
del.sectionGeneration++;
|
|
[cv reloadSections:[NSIndexSet indexSetWithIndex:sectionToReload]];
|
|
[cv waitUntilAllUpdatesAreCommitted];
|
|
|
|
NSInteger sectionCount = del->_itemCounts.size();
|
|
for (NSInteger section = 0; section < sectionCount; section++) {
|
|
ASTestSectionContext *context = (ASTestSectionContext *)[cn contextForSection:section];
|
|
XCTAssertNotNil(context);
|
|
XCTAssertEqual(context.sectionGeneration, section != sectionToReload ? 1 : 2);
|
|
XCTAssertEqual(context.sectionIndex, section);
|
|
}
|
|
}
|
|
|
|
/// See the same test in ASUICollectionViewTests for the reference behavior.
|
|
- (void)testThatIssuingAnUpdateBeforeInitialReloadIsAcceptable
|
|
{
|
|
ASCollectionViewTestDelegate *del = [[ASCollectionViewTestDelegate alloc] initWithNumberOfSections:0 numberOfItemsInSection:0];
|
|
ASCollectionView *cv = [[ASCollectionView alloc] initWithCollectionViewLayout:[UICollectionViewFlowLayout new]];
|
|
cv.asyncDataSource = del;
|
|
cv.asyncDelegate = del;
|
|
|
|
// Add a section to the data source
|
|
del->_itemCounts.push_back(0);
|
|
// Attempt to insert section into collection view. We ignore it to workaround
|
|
// the bug demonstrated by
|
|
// ASUICollectionViewTests.testThatIssuingAnUpdateBeforeInitialReloadIsUnacceptable
|
|
XCTAssertNoThrow([cv insertSections:[NSIndexSet indexSetWithIndex:0]]);
|
|
}
|
|
|
|
- (void)testThatNodeAtIndexPathIsCorrectImmediatelyAfterSubmittingUpdate
|
|
{
|
|
updateValidationTestPrologue
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
|
|
|
|
// Insert an item and assert nodeForItemAtIndexPath: immediately returns new node
|
|
ASCellNode *oldNode = [cn nodeForItemAtIndexPath:indexPath];
|
|
XCTAssertNotNil(oldNode);
|
|
del->_itemCounts[0] += 1;
|
|
[cv insertItemsAtIndexPaths:@[ indexPath ]];
|
|
ASCellNode *newNode = [cn nodeForItemAtIndexPath:indexPath];
|
|
XCTAssertNotNil(newNode);
|
|
XCTAssertNotEqualObjects(oldNode, newNode);
|
|
|
|
// Delete all sections and assert nodeForItemAtIndexPath: immediately returns nil
|
|
NSIndexSet *sections = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, del->_itemCounts.size())];
|
|
del->_itemCounts.clear();
|
|
[cv deleteSections:sections];
|
|
XCTAssertNil([cn nodeForItemAtIndexPath:indexPath]);
|
|
}
|
|
|
|
- (void)DISABLED_testThatSupplementaryNodeAtIndexPathIsCorrectImmediatelyAfterSubmittingUpdate
|
|
{
|
|
updateValidationTestPrologue
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
|
|
ASCellNode *oldHeader = [cv supplementaryNodeForElementKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
|
|
XCTAssertNotNil(oldHeader);
|
|
|
|
// Reload the section and ensure that the new header is loaded
|
|
[cv reloadSections:[NSIndexSet indexSetWithIndex:0]];
|
|
ASCellNode *newHeader = [cv supplementaryNodeForElementKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
|
|
XCTAssertNotNil(newHeader);
|
|
XCTAssertNotEqualObjects(oldHeader, newHeader);
|
|
}
|
|
|
|
- (void)testThatNilBatchUpdatesCanBeSubmitted
|
|
{
|
|
__block ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
__block ASCollectionNode *cn = testController.collectionNode;
|
|
|
|
// Passing nil blocks should not crash
|
|
[cn performBatchUpdates:nil completion:nil];
|
|
[cn performBatchAnimated:NO updates:nil completion:nil];
|
|
}
|
|
|
|
- (void)testThatDeletedItemsAreMarkedInvisible
|
|
{
|
|
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
|
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
window.rootViewController = testController;
|
|
|
|
__block NSInteger itemCount = 1;
|
|
testController.asyncDelegate->_itemCounts = {itemCount};
|
|
[window makeKeyAndVisible];
|
|
[window layoutIfNeeded];
|
|
|
|
ASCollectionNode *cn = testController.collectionNode;
|
|
[cn waitUntilAllUpdatesAreProcessed];
|
|
[cn.view layoutIfNeeded];
|
|
ASCellNode *node = [cn nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
|
|
ASCATransactionQueueWait(nil);
|
|
XCTAssertTrue(node.visible);
|
|
testController.asyncDelegate->_itemCounts = {0};
|
|
[cn deleteItemsAtIndexPaths: @[[NSIndexPath indexPathForItem:0 inSection:0]]];
|
|
[self expectationForPredicate:[NSPredicate predicateWithFormat:@"visible = NO"] evaluatedWithObject:node handler:nil];
|
|
[self waitForExpectationsWithTimeout:3 handler:nil];
|
|
}
|
|
|
|
- (void)disabled_testThatMultipleBatchFetchesDontHappenUnnecessarily
|
|
{
|
|
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
|
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
window.rootViewController = testController;
|
|
|
|
// Start with 1 item so that our content does not fill bounds.
|
|
__block NSInteger itemCount = 1;
|
|
testController.asyncDelegate->_itemCounts = {itemCount};
|
|
[window makeKeyAndVisible];
|
|
[window layoutIfNeeded];
|
|
|
|
ASCollectionNode *cn = testController.collectionNode;
|
|
[cn waitUntilAllUpdatesAreProcessed];
|
|
XCTAssertGreaterThan(cn.bounds.size.height, cn.view.contentSize.height, @"Expected initial data not to fill collection view area.");
|
|
|
|
__block NSUInteger batchFetchCount = 0;
|
|
XCTestExpectation *expectation = [self expectationWithDescription:@"Batch fetching completed and then some"];
|
|
__weak ASCollectionViewTestController *weakController = testController;
|
|
testController.asyncDelegate.willBeginBatchFetch = ^(ASBatchContext *context) {
|
|
|
|
// Ensure only 1 batch fetch happens
|
|
batchFetchCount += 1;
|
|
if (batchFetchCount > 1) {
|
|
XCTFail(@"Too many batch fetches!");
|
|
return;
|
|
}
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Up the item count to 1000 so that we're well beyond the
|
|
// edge of the collection view and not ready for another batch fetch.
|
|
NSMutableArray *indexPaths = [NSMutableArray array];
|
|
for (; itemCount < 1000; itemCount++) {
|
|
[indexPaths addObject:[NSIndexPath indexPathForItem:itemCount inSection:0]];
|
|
}
|
|
weakController.asyncDelegate->_itemCounts = {itemCount};
|
|
[cn insertItemsAtIndexPaths:indexPaths];
|
|
[context completeBatchFetching:YES];
|
|
|
|
// Let the run loop turn before we consider the test passed.
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[expectation fulfill];
|
|
});
|
|
});
|
|
};
|
|
[self waitForExpectationsWithTimeout:3 handler:nil];
|
|
}
|
|
|
|
- (void)testThatBatchFetchHappensForEmptyCollection
|
|
{
|
|
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
|
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
window.rootViewController = testController;
|
|
|
|
testController.asyncDelegate->_itemCounts = {};
|
|
[window makeKeyAndVisible];
|
|
[window layoutIfNeeded];
|
|
|
|
ASCollectionNode *cn = testController.collectionNode;
|
|
[cn waitUntilAllUpdatesAreProcessed];
|
|
|
|
__block NSUInteger batchFetchCount = 0;
|
|
XCTestExpectation *e = [self expectationWithDescription:@"Batch fetching completed"];
|
|
testController.asyncDelegate.willBeginBatchFetch = ^(ASBatchContext *context) {
|
|
// Ensure only 1 batch fetch happens
|
|
batchFetchCount += 1;
|
|
if (batchFetchCount > 1) {
|
|
XCTFail(@"Too many batch fetches!");
|
|
return;
|
|
}
|
|
[e fulfill];
|
|
};
|
|
[self waitForExpectationsWithTimeout:3 handler:nil];
|
|
}
|
|
|
|
- (void)testThatWeBatchFetchUntilContentRequirementIsMet_Animated
|
|
{
|
|
[self _primitiveBatchFetchingFillTestAnimated:YES visible:YES controller:nil];
|
|
}
|
|
|
|
- (void)testThatWeBatchFetchUntilContentRequirementIsMet_Nonanimated
|
|
{
|
|
[self _primitiveBatchFetchingFillTestAnimated:NO visible:YES controller:nil];
|
|
}
|
|
|
|
- (void)testThatWeBatchFetchUntilContentRequirementIsMet_Invisible
|
|
{
|
|
[self _primitiveBatchFetchingFillTestAnimated:NO visible:NO controller:nil];
|
|
}
|
|
|
|
- (void)testThatWhenWeBecomeVisibleWeWillFetchAdditionalContent
|
|
{
|
|
ASCollectionViewTestController *ctrl = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
// Start with 1 empty section
|
|
ctrl.asyncDelegate->_itemCounts = {0};
|
|
[self _primitiveBatchFetchingFillTestAnimated:NO visible:NO controller:ctrl];
|
|
XCTAssertGreaterThan([ctrl.collectionNode numberOfItemsInSection:0], 0);
|
|
[self _primitiveBatchFetchingFillTestAnimated:NO visible:YES controller:ctrl];
|
|
}
|
|
|
|
- (void)_primitiveBatchFetchingFillTestAnimated:(BOOL)animated visible:(BOOL)visible controller:(nullable ASCollectionViewTestController *)testController
|
|
{
|
|
if (testController == nil) {
|
|
testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
// Start with 1 empty section
|
|
testController.asyncDelegate->_itemCounts = {0};
|
|
}
|
|
ASCollectionNode *cn = testController.collectionNode;
|
|
|
|
UIWindow *window = nil;
|
|
UIView *view = nil;
|
|
if (visible) {
|
|
window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
|
view = window;
|
|
} else {
|
|
view = cn.view;
|
|
view.frame = [UIScreen mainScreen].bounds;
|
|
}
|
|
|
|
XCTestExpectation *expectation = [self expectationWithDescription:@"Completed all batch fetches"];
|
|
__weak ASCollectionViewTestController *weakController = testController;
|
|
__block NSInteger batchFetchCount = 0;
|
|
testController.asyncDelegate.willBeginBatchFetch = ^(ASBatchContext *context) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSInteger fetchIndex = batchFetchCount++;
|
|
|
|
NSInteger itemCount = weakController.asyncDelegate->_itemCounts[0];
|
|
weakController.asyncDelegate->_itemCounts[0] = (itemCount + 1);
|
|
if (animated) {
|
|
[cn insertItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:itemCount inSection:0] ]];
|
|
} else {
|
|
[cn performBatchAnimated:NO updates:^{
|
|
[cn insertItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:itemCount inSection:0] ]];
|
|
} completion:nil];
|
|
}
|
|
|
|
[context completeBatchFetching:YES];
|
|
|
|
// If no more batch fetches have happened in 1 second, assume we're done.
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
if (fetchIndex == batchFetchCount - 1) {
|
|
[expectation fulfill];
|
|
}
|
|
});
|
|
});
|
|
};
|
|
window.rootViewController = testController;
|
|
|
|
[window makeKeyAndVisible];
|
|
// Trigger the initial reload to start
|
|
[view layoutIfNeeded];
|
|
|
|
// Wait for ASDK reload to finish
|
|
[cn waitUntilAllUpdatesAreProcessed];
|
|
// Force UIKit to read updated data & range controller to update and account for it
|
|
[cn.view layoutIfNeeded];
|
|
[self waitForExpectationsWithTimeout:60 handler:nil];
|
|
|
|
CGFloat contentHeight = cn.view.contentSize.height;
|
|
CGFloat requiredContentHeight;
|
|
CGFloat itemHeight = [cn.view layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]].size.height;
|
|
if (visible) {
|
|
requiredContentHeight = CGRectGetMaxY(cn.bounds) + CGRectGetHeight(cn.bounds) * cn.view.leadingScreensForBatching;
|
|
} else {
|
|
requiredContentHeight = CGRectGetMaxY(cn.bounds);
|
|
}
|
|
XCTAssertGreaterThan(batchFetchCount, 2);
|
|
XCTAssertGreaterThanOrEqual(contentHeight, requiredContentHeight, @"Loaded too little content.");
|
|
XCTAssertLessThanOrEqual(contentHeight, requiredContentHeight + 3 * itemHeight, @"Loaded too much content.");
|
|
}
|
|
|
|
- (void)testInitialRangeBounds
|
|
{
|
|
[self testInitialRangeBoundsWithCellLayoutMode:ASCellLayoutModeNone
|
|
shouldWaitUntilAllUpdatesAreProcessed:YES];
|
|
}
|
|
|
|
- (void)testInitialRangeBoundsCellLayoutModeAlwaysAsync
|
|
{
|
|
[self testInitialRangeBoundsWithCellLayoutMode:ASCellLayoutModeAlwaysAsync
|
|
shouldWaitUntilAllUpdatesAreProcessed:YES];
|
|
}
|
|
|
|
- (void)testInitialRangeBoundsWithCellLayoutMode:(ASCellLayoutMode)cellLayoutMode
|
|
shouldWaitUntilAllUpdatesAreProcessed:(BOOL)shouldWait
|
|
{
|
|
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
|
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
ASCollectionNode *cn = testController.collectionNode;
|
|
cn.cellLayoutMode = cellLayoutMode;
|
|
[cn setTuningParameters:{ .leadingBufferScreenfuls = 2, .trailingBufferScreenfuls = 0 } forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload];
|
|
window.rootViewController = testController;
|
|
|
|
[testController.collectionNode.collectionViewLayout invalidateLayout];
|
|
[testController.collectionNode.collectionViewLayout prepareLayout];
|
|
|
|
[window makeKeyAndVisible];
|
|
// Trigger the initial reload to start
|
|
[window layoutIfNeeded];
|
|
|
|
if (shouldWait) {
|
|
XCTAssertTrue(cn.isProcessingUpdates, @"ASCollectionNode should still be processing updates after initial layoutIfNeeded call (reloadData)");
|
|
|
|
[cn onDidFinishProcessingUpdates:^{
|
|
XCTAssertTrue(!cn.isProcessingUpdates, @"ASCollectionNode should no longer be processing updates inside -onDidFinishProcessingUpdates: block");
|
|
}];
|
|
|
|
// Wait for ASDK reload to finish
|
|
[cn waitUntilAllUpdatesAreProcessed];
|
|
}
|
|
|
|
XCTAssertTrue(!cn.isProcessingUpdates, @"ASCollectionNode should no longer be processing updates after -wait call");
|
|
|
|
// Force UIKit to read updated data & range controller to update and account for it
|
|
[cn.view layoutIfNeeded];
|
|
|
|
CGRect preloadBounds = ({
|
|
CGRect r = CGRectNull;
|
|
for (NSInteger s = 0; s < cn.numberOfSections; s++) {
|
|
NSInteger c = [cn numberOfItemsInSection:s];
|
|
for (NSInteger i = 0; i < c; i++) {
|
|
NSIndexPath *ip = [NSIndexPath indexPathForItem:i inSection:s];
|
|
ASCellNode *node = [cn nodeForItemAtIndexPath:ip];
|
|
ASCATransactionQueueWait(nil);
|
|
if (node.inPreloadState) {
|
|
CGRect frame = [cn.view layoutAttributesForItemAtIndexPath:ip].frame;
|
|
r = CGRectUnion(r, frame);
|
|
}
|
|
}
|
|
}
|
|
r;
|
|
});
|
|
CGFloat expectedHeight = cn.bounds.size.height * 3;
|
|
XCTAssertEqualWithAccuracy(CGRectGetHeight(preloadBounds), expectedHeight, expectedHeight * 0.1);
|
|
XCTAssertEqual([[cn valueForKeyPath:@"rangeController.currentRangeMode"] integerValue], ASLayoutRangeModeMinimum, @"Expected range mode to be minimum before scrolling begins.");
|
|
}
|
|
|
|
- (void)testTraitCollectionChangesMidUpdate
|
|
{
|
|
CGRect screenBounds = [UIScreen mainScreen].bounds;
|
|
UIWindow *window = [[UIWindow alloc] initWithFrame:screenBounds];
|
|
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
ASCollectionNode *cn = testController.collectionNode;
|
|
window.rootViewController = testController;
|
|
|
|
[window makeKeyAndVisible];
|
|
// Trigger the initial reload to start
|
|
[window layoutIfNeeded];
|
|
|
|
// The initial reload is async, changing the trait collection here should be "mid-update"
|
|
ASPrimitiveTraitCollection traitCollection = ASPrimitiveTraitCollectionMakeDefault();
|
|
traitCollection.displayScale = cn.primitiveTraitCollection.displayScale + 1; // Just a dummy change
|
|
traitCollection.containerSize = screenBounds.size;
|
|
cn.primitiveTraitCollection = traitCollection;
|
|
|
|
[cn waitUntilAllUpdatesAreProcessed];
|
|
[cn.view layoutIfNeeded];
|
|
|
|
// Assert that the new trait collection is picked up by all cell nodes, including ones that were not allocated but are forced to allocate now
|
|
for (NSInteger s = 0; s < cn.numberOfSections; s++) {
|
|
NSInteger c = [cn numberOfItemsInSection:s];
|
|
for (NSInteger i = 0; i < c; i++) {
|
|
NSIndexPath *ip = [NSIndexPath indexPathForItem:i inSection:s];
|
|
ASCellNode *node = [cn.view nodeForItemAtIndexPath:ip];
|
|
XCTAssertTrue(ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(traitCollection, node.primitiveTraitCollection));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This tests an issue where, since subnode insertions aren't applied until the UIKit layout pass,
|
|
* which we trigger during the display phase, subnodes like network image nodes are not preloading
|
|
* until this layout pass happens which is too late.
|
|
*/
|
|
- (void)DISABLED_testThatAutomaticallyManagedSubnodesGetPreloadCallBeforeDisplay
|
|
{
|
|
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
|
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
|
|
window.rootViewController = testController;
|
|
ASCollectionNode *cn = testController.collectionNode;
|
|
|
|
__block NSInteger itemCount = 100;
|
|
testController.asyncDelegate->_itemCounts = {itemCount};
|
|
[window makeKeyAndVisible];
|
|
[window layoutIfNeeded];
|
|
|
|
[cn waitUntilAllUpdatesAreProcessed];
|
|
for (NSInteger i = 0; i < itemCount; i++) {
|
|
ASTextCellNodeWithSetSelectedCounter *node = [cn nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
|
|
XCTAssert(node.automaticallyManagesSubnodes, @"Expected test cell node to use automatic subnode management. Can modify the test with a different class if needed.");
|
|
ASDisplayNode *subnode = node.textNode;
|
|
XCTAssertEqualObjects(NSStringFromASInterfaceState(subnode.interfaceState), NSStringFromASInterfaceState(node.interfaceState), @"Subtree interface state should match cell node interface state for ASM nodes.");
|
|
XCTAssert(node.inDisplayState || !node.nodeLoaded, @"Only nodes in the display range should be loaded.");
|
|
}
|
|
|
|
}
|
|
|
|
@end
|