ComponentKitTests/CKComponentViewManagerTests.mm (243 lines of code) (raw):
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <XCTest/XCTest.h>
#import <ComponentKit/ComponentViewManager.h>
#import <ComponentKit/ComponentViewReuseUtilities.h>
#import <ComponentKit/CKCasting.h>
#import <ComponentKit/CKComponent.h>
#import <ComponentKit/CKComponentInternal.h>
#import <ComponentKit/CKCompositeComponent.h>
#import "CKComponentTestCase.h"
using CK::Component::ViewManager;
@interface CKComponentViewManagerTests : CKComponentTestCase
@end
/** Overrides all subview related methods *except* addSubview: to throw. */
@interface CKAddSubviewOnlyView : UIView
@property (nonatomic, assign) NSUInteger numberOfSubviewsAdded;
@end
/** View provides `didEnterReusePool` callback */
@interface CKTestReusableView : UIView
@property (nonatomic, readonly, assign) BOOL isDidEnterReusePoolCalled;
- (void)didEnterReusePool;
@end
@implementation CKComponentViewManagerTests
- (void)testThatComponentViewManagerVendsRecycledView
{
CKComponent *component = CK::ComponentBuilder()
.viewClass([UIView class])
.build();
UIView *container = [[UIView alloc] init];
CK::Component::ViewReuseUtilities::mountingInRootView(container);
UIView *subview;
{
ViewManager m(container);
subview = m.viewForConfiguration([component class], [component viewConfiguration]);
}
{
ViewManager m(container);
XCTAssertTrue(subview == m.viewForConfiguration([component class], [component viewConfiguration]), @"Expected to receive recycled view");
}
}
- (void)testThatComponentViewManagerHidesViewIfItWasNotRecycled
{
CKComponent *component = CK::ComponentBuilder()
.viewClass([UIView class])
.build();
UIView *container = [[UIView alloc] init];
CK::Component::ViewReuseUtilities::mountingInRootView(container);
UIView *subview;
{
ViewManager m(container);
subview = m.viewForConfiguration([component class], [component viewConfiguration]);
}
XCTAssertFalse(subview.hidden, @"Did not expect subview to be hidden when it is initially vended");
{
ViewManager m(container);
}
XCTAssertTrue(subview.hidden, @"Expected subview to be hidden since it was not vended from the ComponentViewManager");
}
static NSArray *arrayByPerformingBlock(NSArray *array, id (^block)(id))
{
NSMutableArray *result = [NSMutableArray array];
for (id obj in array) {
id res = block(obj);
if (res != nil) {
[result addObject:res];
}
}
return result;
}
- (void)testThatComponentViewManagerReordersViewsIfOrderSwapped
{
CKComponent *imageView = CK::ComponentBuilder()
.viewClass([UIImageView class])
.build();
CKComponent *button = CK::ComponentBuilder()
.viewClass([UIButton class])
.build();
NSArray *actualClasses, *expectedClasses;
UIView *container = [[UIView alloc] init];
CK::Component::ViewReuseUtilities::mountingInRootView(container);
{
ViewManager m(container);
m.viewForConfiguration([imageView class], [imageView viewConfiguration]);
m.viewForConfiguration([button class], [button viewConfiguration]);
}
actualClasses = arrayByPerformingBlock([container subviews], ^id(id object) { return [object class]; });
expectedClasses = @[[UIImageView class], [UIButton class]];
XCTAssertEqualObjects(actualClasses, expectedClasses, @"Expected imageview then button");
{
ViewManager m(container);
m.viewForConfiguration([button class], [button viewConfiguration]);
m.viewForConfiguration([imageView class], [imageView viewConfiguration]);
}
actualClasses = arrayByPerformingBlock([container subviews], ^id(id object) { return [object class]; });
expectedClasses = @[[UIButton class], [UIImageView class]];
XCTAssertEqualObjects(actualClasses, expectedClasses, @"Expected button then image view");
}
- (void)testThatComponentViewManagerDoesNotUnnecessarilyReorderViews
{
CKComponent *imageView = CK::ComponentBuilder()
.viewClass([UIImageView class])
.build();
CKComponent *button = CK::ComponentBuilder()
.viewClass([UIButton class])
.build();
CKAddSubviewOnlyView *container = [[CKAddSubviewOnlyView alloc] init];
CK::Component::ViewReuseUtilities::mountingInRootView(container);
{
ViewManager m(container);
m.viewForConfiguration([imageView class], [imageView viewConfiguration]);
m.viewForConfiguration([button class], [button viewConfiguration]);
}
{
ViewManager m(container);
m.viewForConfiguration([imageView class], [imageView viewConfiguration]);
m.viewForConfiguration([button class], [button viewConfiguration]);
}
XCTAssertEqual(container.numberOfSubviewsAdded, 2u, @"Expected exactly two subviews to be added");
}
- (void)testThatGettingRecycledViewForComponentDoesNotRecycleViewWithDisjointAttributes
{
CKComponent *bgColorComponent =
CK::ComponentBuilder()
.viewClass([UIView class])
.backgroundColor([UIColor blueColor])
.build();
UIView *container = [[UIView alloc] init];
CK::Component::ViewReuseUtilities::mountingInRootView(container);
UIView *subview;
{
ViewManager m(container);
subview = m.viewForConfiguration([bgColorComponent class], [bgColorComponent viewConfiguration]);
}
CKComponent *alphaComponent =
CK::ComponentBuilder()
.viewClass([UIView class])
.alpha(0.5)
.build();
{
ViewManager m(container);
XCTAssertTrue(subview != m.viewForConfiguration([alphaComponent class], [alphaComponent viewConfiguration]), @"Did not expect to receive recycled view with a disjoint attribute set; it would have a blue background that is not reset");
XCTAssertTrue(subview == m.viewForConfiguration([bgColorComponent class], [bgColorComponent viewConfiguration]), @"Did expect that the view would be recycled when requested with a matching attribute set");
}
}
static UIView *imageViewFactory()
{
return [[UIImageView alloc] init];
}
- (void)testThatGettingViewForViewComponentWithNilViewClassCallsClassMethodNewView
{
CKComponentViewClass customClass(&imageViewFactory);
CKComponent *testComponent = CK::ComponentBuilder()
.viewClass(std::move(customClass))
.build();
UIView *container = [[UIView alloc] init];
CK::Component::ViewReuseUtilities::mountingInRootView(container);
ViewManager m(container);
UIView *subview = m.viewForConfiguration([testComponent class], [testComponent viewConfiguration]);
XCTAssertTrue([subview isKindOfClass:[UIImageView class]], @"Expected +newView to vend a UIImageView");
}
- (void)testThatViewsInViewPoolAreHiddenAndDidHideIsCalledInDescendantAfterHideAllIsCalledOnRootView
{
CKComponent *childComponent =
CK::ComponentBuilder()
.viewClass({[CKTestReusableView class], @selector(didEnterReusePool), nil})
.build();
CKComponent *component =
CK::CompositeComponentBuilder()
.viewClass({[CKTestReusableView class], @selector(didEnterReusePool), nil})
.component(childComponent)
.build();
UIView *container = [[UIView alloc] init];
CK::Component::ViewReuseUtilities::mountingInRootView(container);
{
ViewManager m1(container);
const auto subview = m1.viewForConfiguration([component class], [component viewConfiguration]);
{
ViewManager m2(subview);
m2.viewForConfiguration([childComponent class], [childComponent viewConfiguration]);
}
}
// All subviews should be visible after view manager is reset
NSInteger numberOfViewsVisible = 0;
checkSubviewsAreHidden(container, NO, &numberOfViewsVisible);
XCTAssertEqual(numberOfViewsVisible, 2);
CK::Component::ViewReusePool::hideAll(container, nullptr);
// Only views in the view pool of `container` are hidden since there is no need to `setHidden` for their descendant.
NSInteger numberOfViewsHidden = 0;
checkSubviewsAreHidden(container, YES, &numberOfViewsHidden);
XCTAssertEqual(numberOfViewsHidden, 1);
// Although `setHidden` is not needed to be called on all descendant, calling `didEnterReusePool` is necessary
// because we need to notify all views in the hierarchy that they did enter reuse pool.
XCTAssertTrue(isDidEnterReusePoolIsCalledOnDescendant(container));
}
static void checkSubviewsAreHidden(UIView *view, BOOL isHidden, NSInteger *numberOfViewsMatched)
{
for (UIView *subview in view.subviews) {
if (subview.isHidden == isHidden) {
(*numberOfViewsMatched)++;
}
checkSubviewsAreHidden(subview, isHidden, numberOfViewsMatched);
}
}
static BOOL isDidEnterReusePoolIsCalledOnDescendant(UIView *view)
{
for (UIView *subview in view.subviews) {
const auto reusableView = CK::objCForceCast<CKTestReusableView>(subview);
if (!reusableView.isDidEnterReusePoolCalled) {
return NO;
}
}
return YES;
}
@end
@implementation CKAddSubviewOnlyView
- (void)addSubview:(UIView *)view
{
_numberOfSubviewsAdded++;
[super addSubview:view];
}
- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2
{
[NSException raise:NSGenericException format:@"Unexpected %@", NSStringFromSelector(_cmd)];
}
- (void)bringSubviewToFront:(UIView *)view
{
[NSException raise:NSGenericException format:@"Unexpected %@", NSStringFromSelector(_cmd)];
}
- (void)sendSubviewToBack:(UIView *)view
{
[NSException raise:NSGenericException format:@"Unexpected %@", NSStringFromSelector(_cmd)];
}
- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview
{
[NSException raise:NSGenericException format:@"Unexpected %@", NSStringFromSelector(_cmd)];
}
- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview
{
[NSException raise:NSGenericException format:@"Unexpected %@", NSStringFromSelector(_cmd)];
}
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index
{
[NSException raise:NSGenericException format:@"Unexpected %@", NSStringFromSelector(_cmd)];
}
@end
@implementation CKTestReusableView
- (void)didEnterReusePool
{
_isDidEnterReusePoolCalled = YES;
}
@end