diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 8a41e85a1..0c294b32b 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -372,6 +372,8 @@ CCA5F62E1EECC2A80060C137 /* ASAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA5F62D1EECC2A80060C137 /* ASAssert.m */; }; CCA5F62C1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA5F62B1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m */; }; CCB2F34D1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */; }; + CCB338E41EEE11160081F21A /* OCMockObject+ASAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB338E31EEE11160081F21A /* OCMockObject+ASAdditions.m */; }; + CCB338E71EEE27760081F21A /* ASTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB338E61EEE27760081F21A /* ASTestCase.m */; }; CCBBBF5D1EB161760069AA91 /* ASRangeManagingNode.h in Headers */ = {isa = PBXBuildFile; fileRef = CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; CCCCCCD51EC3EF060087FE10 /* ASTextDebugOption.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC31EC3EF060087FE10 /* ASTextDebugOption.h */; }; CCCCCCD61EC3EF060087FE10 /* ASTextDebugOption.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCC41EC3EF060087FE10 /* ASTextDebugOption.m */; }; @@ -393,6 +395,7 @@ CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */; }; CCD523111EBD658C001F2191 /* ASTextNode2.h in Headers */ = {isa = PBXBuildFile; fileRef = CCD5230F1EBD658C001F2191 /* ASTextNode2.h */; }; CCD523121EBD658C001F2191 /* ASTextNode2.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCD523101EBD658C001F2191 /* ASTextNode2.mm */; }; + CCDD148B1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */; }; CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */; settings = {ATTRIBUTES = (Private, ); }; }; DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; @@ -831,6 +834,10 @@ CCA5F62A1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+ASTestHelpers.h"; sourceTree = ""; }; CCA5F62B1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSInvocation+ASTestHelpers.m"; sourceTree = ""; }; CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeSnapshotTests.m; sourceTree = ""; }; + CCB338E21EEE11160081F21A /* OCMockObject+ASAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCMockObject+ASAdditions.h"; sourceTree = ""; }; + CCB338E31EEE11160081F21A /* OCMockObject+ASAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OCMockObject+ASAdditions.m"; sourceTree = ""; }; + CCB338E51EEE27760081F21A /* ASTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTestCase.h; sourceTree = ""; }; + CCB338E61EEE27760081F21A /* ASTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTestCase.m; sourceTree = ""; }; CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeManagingNode.h; sourceTree = ""; }; CCBD05DE1E4147B000D18509 /* ASIGListAdapterBasedDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIGListAdapterBasedDataSource.m; sourceTree = ""; }; CCBD05DF1E4147B000D18509 /* ASIGListAdapterBasedDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIGListAdapterBasedDataSource.h; sourceTree = ""; }; @@ -854,6 +861,7 @@ CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+ASText.m"; sourceTree = ""; }; CCD5230F1EBD658C001F2191 /* ASTextNode2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextNode2.h; sourceTree = ""; }; CCD523101EBD658C001F2191 /* ASTextNode2.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNode2.mm; sourceTree = ""; }; + CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionModernDataSourceTests.m; sourceTree = ""; }; CCE04B1E1E313EA7006AEBBB /* ASSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSectionController.h; sourceTree = ""; }; CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListAdapter+AsyncDisplayKit.h"; sourceTree = ""; }; CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapter+AsyncDisplayKit.m"; sourceTree = ""; }; @@ -1109,8 +1117,13 @@ 058D09C5195D04C000B7D73C /* Tests */ = { isa = PBXGroup; children = ( + CCB338E51EEE27760081F21A /* ASTestCase.h */, + CCB338E61EEE27760081F21A /* ASTestCase.m */, + CCB338E21EEE11160081F21A /* OCMockObject+ASAdditions.h */, + CCB338E31EEE11160081F21A /* OCMockObject+ASAdditions.m */, CCA5F62A1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.h */, CCA5F62B1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m */, + CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */, CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */, CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, @@ -2053,6 +2066,7 @@ CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */, CC54A81E1D7008B300296A24 /* ASDispatchTests.m in Sources */, 058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */, + CCB338E71EEE27760081F21A /* ASTestCase.m in Sources */, 83A7D95E1D446A6E00BF333E /* ASWeakMapTests.m in Sources */, 056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.m in Sources */, AC026B581BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.m in Sources */, @@ -2063,12 +2077,14 @@ 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m in Sources */, CC8B05D81D73979700F54286 /* ASTextNodePerformanceTests.m in Sources */, 697B315A1CFE4B410049936F /* ASEditableTextNodeTests.m in Sources */, + CCB338E41EEE11160081F21A /* OCMockObject+ASAdditions.m in Sources */, ACF6ED611B178DC700DA7C62 /* ASOverlayLayoutSpecSnapshotTests.mm in Sources */, CC8B05D61D73836400F54286 /* ASPerformanceTestContext.m in Sources */, CC0AEEA41D66316E005D1C78 /* ASUICollectionViewTests.m in Sources */, 69B225671D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm in Sources */, ACF6ED621B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm in Sources */, 7AB338691C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm in Sources */, + CCDD148B1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m in Sources */, 254C6B541BF8FF2A003EC431 /* ASTextKitTests.mm in Sources */, 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.m in Sources */, ACF6ED631B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm in Sources */, diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 696befb60..e3a95cd40 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -1647,7 +1647,7 @@ - (ASCellNodeBlock)dataController:(ASDataController *)dataController nodeBlockAt return cell; }; } else { - ASDisplayNodeFailAssert(@"ASCollection could not get a node block for row at index path %@: %@, %@. If you are trying to display a UICollectionViewCell, make sure your dataSource conforms to the protocol!", indexPath, cell, block); + ASDisplayNodeFailAssert(@"ASCollection could not get a node block for item at index path %@: %@, %@. If you are trying to display a UICollectionViewCell, make sure your dataSource conforms to the protocol!", indexPath, cell, block); block = ^{ return [[ASCellNode alloc] init]; }; diff --git a/Tests/ASCollectionModernDataSourceTests.m b/Tests/ASCollectionModernDataSourceTests.m new file mode 100644 index 000000000..e7ba88140 --- /dev/null +++ b/Tests/ASCollectionModernDataSourceTests.m @@ -0,0 +1,106 @@ +// +// ASCollectionModernDataSourceTests.m +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import "OCMockObject+ASAdditions.h" +#import "ASTestCase.h" + +@interface ASCollectionModernDataSourceTests : ASTestCase + +@end + +@implementation ASCollectionModernDataSourceTests { +@private + id mockDataSource; + UIWindow *window; + UIViewController *viewController; + ASCollectionNode *collectionNode; +} + +- (void)setUp { + [super setUp]; + window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + viewController = [[UIViewController alloc] init]; + + window.rootViewController = viewController; + [window makeKeyAndVisible]; + collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:[UICollectionViewFlowLayout new]]; + collectionNode.frame = viewController.view.bounds; + collectionNode.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [viewController.view addSubnode:collectionNode]; + + mockDataSource = OCMStrictProtocolMock(@protocol(ASCollectionDataSource)); + [mockDataSource addImplementedOptionalProtocolMethods: + @selector(numberOfSectionsInCollectionNode:), + @selector(collectionNode:numberOfItemsInSection:), + @selector(collectionNode:nodeBlockForItemAtIndexPath:), + nil]; + [mockDataSource setExpectationOrderMatters:YES]; + + // NOTE: Adding optionally-implemented methods after this point won't work due to ASCollectionNode selector caching. + collectionNode.dataSource = mockDataSource; +} + +- (void)tearDown +{ + OCMVerifyAll(mockDataSource); + [super tearDown]; +} + +- (void)testInitialDataLoadingCallPattern +{ + /// BUG: these methods are called twice in a row i.e. this for-loop shouldn't be here. https://github.com/TextureGroup/Texture/issues/351 + for (int i = 0; i < 2; i++) { + NSArray *counts = @[ @2 ]; + [self expectDataSourceMethodsWithCounts:counts]; + } + + [window layoutIfNeeded]; +} + +#pragma mark - Helpers + +/** + * Adds expectations for the sequence: + * + * numberOfSectionsInCollectionNode: + * for section in countsArray + * numberOfItemsInSection: + * for item < itemCount + * nodeBlockForItemAtIndexPath: + */ +- (void)expectDataSourceMethodsWithCounts:(NSArray *)counts +{ + // -numberOfSectionsInCollectionNode + OCMExpect([mockDataSource numberOfSectionsInCollectionNode:collectionNode]) + .andReturn(counts.count); + + // For each section: + // Note: Skip fast enumeration for readability. + for (NSInteger section = 0; section < counts.count; section++) { + NSInteger itemCount = counts[section].integerValue; + OCMExpect([mockDataSource collectionNode:collectionNode numberOfItemsInSection:section]) + .andReturn(itemCount); + + // For each item: + for (NSInteger i = 0; i < itemCount; i++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section]; + OCMExpect([mockDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath]) + .andReturn((ASCellNodeBlock)^{ return [[ASCellNode alloc] init]; }); + } + } +} + +@end diff --git a/Tests/ASTestCase.h b/Tests/ASTestCase.h new file mode 100644 index 000000000..c023abb8f --- /dev/null +++ b/Tests/ASTestCase.h @@ -0,0 +1,17 @@ +// +// ASTestCase.h +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASTestCase : XCTestCase + +@end diff --git a/Tests/ASTestCase.m b/Tests/ASTestCase.m new file mode 100644 index 000000000..854179d3c --- /dev/null +++ b/Tests/ASTestCase.m @@ -0,0 +1,50 @@ +// +// ASTestCase.m +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTestCase.h" +#import + +@implementation ASTestCase + +- (void)tearDown +{ + // Clear out all application windows. Note: the system will retain these sometimes on its + // own but we'll do our best. + for (UIWindow *window in [UIApplication sharedApplication].windows) { + [window resignKeyWindow]; + window.hidden = YES; + for (UIView *view in window.subviews) { + [view removeFromSuperview]; + } + } + + // Set nil for all our subclasses' ivars. Use setValue:forKey: so memory is managed correctly. + Class c = [self class]; + while (c != [ASTestCase class]) { + unsigned int ivarCount; + Ivar *ivars = class_copyIvarList(c, &ivarCount); + for (unsigned int i = 0; i < ivarCount; i++) { + Ivar ivar = ivars[i]; + NSString *key = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding]; + [self setValue:nil forKey:key]; + } + if (ivars) { + free(ivars); + } + + c = [c superclass]; + } + + [super tearDown]; +} + +@end diff --git a/Tests/OCMockObject+ASAdditions.h b/Tests/OCMockObject+ASAdditions.h new file mode 100644 index 000000000..b2966922a --- /dev/null +++ b/Tests/OCMockObject+ASAdditions.h @@ -0,0 +1,29 @@ +// +// OCMockObject+ASAdditions.h +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface OCMockObject (ASAdditions) + +/** + * A method to manually specify which optional protocol methods should return YES + * from -respondsToSelector:. + * + * If you don't call this method, the default OCMock behavior is to + * "implement" all optional protocol methods, which makes it impossible to + * test scenarios where only a subset of optional protocol methods are implemented. + * + * You should only call this on protocol mocks. + */ +- (void)addImplementedOptionalProtocolMethods:(SEL)aSelector, ... NS_REQUIRES_NIL_TERMINATION; + +@end diff --git a/Tests/OCMockObject+ASAdditions.m b/Tests/OCMockObject+ASAdditions.m new file mode 100644 index 000000000..7e7529f51 --- /dev/null +++ b/Tests/OCMockObject+ASAdditions.m @@ -0,0 +1,123 @@ +// +// OCMockObject+ASAdditions.m +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "ASInternalHelpers.h" +#import + +@implementation OCMockObject (ASAdditions) + ++ (void)load +{ + // Swap [OCProtocolMockObject respondsToSelector:] with [(self) swizzled_protocolMockRespondsToSelector:] + Method orig = class_getInstanceMethod(OCMockObject.protocolMockObjectClass, @selector(respondsToSelector:)); + Method new = class_getInstanceMethod(self, @selector(swizzled_protocolMockRespondsToSelector:)); + method_exchangeImplementations(orig, new); +} + +/// Since OCProtocolMockObject is private, use this method to get the class. ++ (Class)protocolMockObjectClass +{ + static Class c; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + c = NSClassFromString(@"OCProtocolMockObject"); + NSAssert(c != Nil, nil); + }); + return c; +} + +/// Whether the user has opted-in to specify which optional methods are implemented for this object. +- (BOOL)hasSpecifiedOptionalProtocolMethods +{ + return objc_getAssociatedObject(self, @selector(optionalImplementedMethods)) != nil; +} + +/// The optional protocol selectors the user has added via -addImplementedOptionalProtocolMethods: +- (NSMutableSet *)optionalImplementedMethods +{ + NSMutableSet *result = objc_getAssociatedObject(self, _cmd); + if (result == nil) { + result = [NSMutableSet set]; + objc_setAssociatedObject(self, _cmd, result, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return result; +} + +- (void)addImplementedOptionalProtocolMethods:(SEL)aSelector, ... +{ + // Can't use isKindOfClass: since we're a proxy. + NSAssert(object_getClass(self) == OCMockObject.protocolMockObjectClass, @"Cannot call this method on non-protocol mocks."); + NSMutableSet *methods = self.optionalImplementedMethods; + + // First arg is not returned by va_arg, needs to be handled separately. + if (aSelector != NULL) { + [methods addObject:NSStringFromSelector(aSelector)]; + } + + va_list args; + va_start(args, aSelector); + SEL s; + while((s = va_arg(args, SEL))) + { + [methods addObject:NSStringFromSelector(s)]; + } + va_end(args); +} + +- (BOOL)implementsOptionalProtocolMethod:(SEL)aSelector +{ + NSAssert(self.hasSpecifiedOptionalProtocolMethods, @"Shouldn't call this method if the user hasn't opted-in to specifying optional protocol methods."); + + // Check our collection first. It'll be in here if they explicitly marked the method as implemented. + for (NSString *str in self.optionalImplementedMethods) { + if (sel_isEqual(NSSelectorFromString(str), aSelector)) { + return YES; + } + } + + // If they didn't explicitly mark it implemented, check if they stubbed/expected it. That counts too, but + // we still want them to have the option to declare that the method exists without + // stubbing it or making an expectation, so the rest of OCMock's mechanisms work as expected. + return [self handleSelector:aSelector]; +} + +- (BOOL)swizzled_protocolMockRespondsToSelector:(SEL)aSelector +{ + // Can't use isKindOfClass: since we're a proxy. + NSAssert(object_getClass(self) == OCMockObject.protocolMockObjectClass, @"Swizzled method should only ever be called for protocol mocks."); + + // If they haven't called our public method to opt-in, use the default behavior. + if (!self.hasSpecifiedOptionalProtocolMethods) { + return [self swizzled_protocolMockRespondsToSelector:aSelector]; + } + + Ivar i = class_getInstanceVariable([self class], "mockedProtocol"); + NSAssert(i != NULL, nil); + Protocol *mockedProtocol = object_getIvar(self, i); + NSAssert(mockedProtocol != NULL, nil); + + // Check if it's an optional protocol method. If not, just return the default implementation (which has now swapped). + struct objc_method_description methodDescription; + methodDescription = protocol_getMethodDescription(mockedProtocol, aSelector, NO, YES); + if (methodDescription.name == NULL) { + methodDescription = protocol_getMethodDescription(mockedProtocol, aSelector, NO, NO); + if (methodDescription.name == NULL) { + return [self swizzled_protocolMockRespondsToSelector:aSelector]; + } + } + + // It's an optional instance or class method. Override the return value. + return [self implementsOptionalProtocolMethod:aSelector]; +} + +@end