ComponentKitTests/CKComponentViewReuseTests.mm (274 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 <ComponentKitTestHelpers/CKComponentLifecycleTestHelper.h> #import <ComponentKit/CKComponent.h> #import <ComponentKit/CKComponentInternal.h> #import <ComponentKit/CKComponentProvider.h> #import <ComponentKit/CKCompositeComponent.h> #import <ComponentKit/ComponentViewManager.h> #import <ComponentKit/ComponentViewReuseUtilities.h> #import "CKComponentTestCase.h" @interface CKComponentViewReuseTests : CKComponentTestCase @end /** Injects a view not controlled by components and specifies its children should be mounted inside it. */ @interface CKViewInjectingComponent : CKCompositeComponent @end /** Doesn't actually do anything, just provides a BOOL for storage. */ @interface CKReuseAwareView : UIView @property (nonatomic, assign, getter=isInReusePool) BOOL inReusePool; @end using namespace CK::Component; @implementation CKComponentViewReuseTests static UIView *viewFactory() { return [[UIView alloc] init]; } - (void)testThatRecyclingViewWithoutEnteringReusePoolDoesNotCallReuseBlocks { CKComponent *component = CK::ComponentBuilder() .viewClass({ &viewFactory, ^(UIView *v){ XCTFail(@"Didn't expect to have didEnterReusePool called"); }, ^(UIView *v){ XCTFail(@"Didn't expect to have willLeaveReusePool called"); } }) .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)testThatViewEnteringReusePoolTriggersCallToDidEnterReusePool { __block UIView *viewThatEnteredReusePool = nil; CKComponent *firstComponent = CK::ComponentBuilder() .viewClass({ &viewFactory, ^(UIView *v){ viewThatEnteredReusePool = v; }, ^(UIView *v){ XCTFail(@"Didn't expect to have willLeaveReusePool called"); } }) .build(); UIView *container = [[UIView alloc] init]; CK::Component::ViewReuseUtilities::mountingInRootView(container); UIView *createdView; { ViewManager m(container); createdView = m.viewForConfiguration([firstComponent class], [firstComponent viewConfiguration]); } CKComponent *secondComponent = CK::ComponentBuilder() .viewClass([UIImageView class]) .build(); { ViewManager m(container); (void)m.viewForConfiguration([secondComponent class], [secondComponent viewConfiguration]); } XCTAssertTrue(viewThatEnteredReusePool == createdView, @"Expected created view %@ to enter pool but got %@", createdView, viewThatEnteredReusePool); } - (void)testThatViewLeavingReusePoolTriggersCallToWillLeaveReusePool { __block UIView *viewThatEnteredReusePool = nil; __block BOOL calledWillLeaveReusePool = NO; CKComponent *firstComponent = CK::ComponentBuilder() .viewClass({ &viewFactory, ^(UIView *v){ viewThatEnteredReusePool = v; }, ^(UIView *v){ XCTAssertTrue(v == viewThatEnteredReusePool, @"Expected %@ but got %@", viewThatEnteredReusePool, v); calledWillLeaveReusePool = YES; } }) .build(); UIView *container = [[UIView alloc] init]; CK::Component::ViewReuseUtilities::mountingInRootView(container); { ViewManager m(container); (void)m.viewForConfiguration([firstComponent class], [firstComponent viewConfiguration]); } CKComponent *secondComponent = CK::ComponentBuilder() .viewClass([UIImageView class]) .build(); { ViewManager m(container); (void)m.viewForConfiguration([secondComponent class], [secondComponent viewConfiguration]); } { ViewManager m(container); (void)m.viewForConfiguration([firstComponent class], [firstComponent viewConfiguration]); } XCTAssertTrue(calledWillLeaveReusePool, @"Expected to call willLeaveReusePool when recycling view"); } - (void)testThatHidingParentViewTriggersCallToDidEnterReusePool { __block UIView *viewThatEnteredReusePool = nil; CKComponent *innerComponent = CK::ComponentBuilder() .viewClass({ &viewFactory, ^(UIView *v){ viewThatEnteredReusePool = v; }, ^(UIView *v){ XCTFail(@"Didn't expect willLeaveReusePool"); } }) .build(); CKComponent *firstComponent = CK::CompositeComponentBuilder() .viewClass([UIView class]) .component(innerComponent) .build(); UIView *container = [[UIView alloc] init]; CK::Component::ViewReuseUtilities::mountingInRootView(container); UIView *topLevelView; { ViewManager m(container); topLevelView = m.viewForConfiguration([firstComponent class], [firstComponent viewConfiguration]); { ViewManager m2(topLevelView); (void)m2.viewForConfiguration([innerComponent class], [innerComponent viewConfiguration]); } } CKComponent *secondComponent = CK::ComponentBuilder() .viewClass([UIImageView class]) .build(); { ViewManager m(container); (void)m.viewForConfiguration([secondComponent class], [secondComponent viewConfiguration]); } XCTAssertNotNil(viewThatEnteredReusePool, @"Expected view to enter reuse pool when its parent was hidden"); XCTAssertFalse(viewThatEnteredReusePool.hidden, @"View that entered pool should not be hidden since its parent was"); XCTAssertTrue(topLevelView.hidden, @"Top-level view should be hidden for reuse"); } - (void)testThatUnhidingParentViewButLeavingChildViewHiddenLeavesViewInReusePool { __block UIView *viewThatEnteredReusePool = nil; CKComponent *innerComponent = CK::ComponentBuilder() .viewClass({ &viewFactory, ^(UIView *v){ viewThatEnteredReusePool = v; }, ^(UIView *v){ XCTFail(@"Didn't expect willLeaveReusePool"); } }) .build(); CKComponent *firstComponent = CK::CompositeComponentBuilder() .viewClass([UIView class]) .component(innerComponent) .build(); UIView *container = [[UIView alloc] init]; CK::Component::ViewReuseUtilities::mountingInRootView(container); UIView *topLevelView; { ViewManager m(container); topLevelView = m.viewForConfiguration([firstComponent class], [firstComponent viewConfiguration]); { ViewManager m2(topLevelView); (void)m2.viewForConfiguration([innerComponent class], [innerComponent viewConfiguration]); } } CKComponent *secondComponent = CK::ComponentBuilder() .viewClass([UIImageView class]) .build(); { ViewManager m(container); (void)m.viewForConfiguration([secondComponent class], [secondComponent viewConfiguration]); } XCTAssertNotNil(viewThatEnteredReusePool, @"Expected view to enter reuse pool when its parent was hidden"); XCTAssertFalse(viewThatEnteredReusePool.hidden, @"View that entered pool should not be hidden since its parent was"); CKComponent *thirdComponent = CK::CompositeComponentBuilder() .viewClass([UIView class]) .component(CK::ComponentBuilder() .build()) .build(); { ViewManager m(container); UIView *newestTopLevelView = m.viewForConfiguration([thirdComponent class], [thirdComponent viewConfiguration]); XCTAssertTrue(newestTopLevelView == topLevelView, @"Expected top level view to be reused"); { ViewManager m2(newestTopLevelView); } } XCTAssertTrue(viewThatEnteredReusePool.hidden, @"View should now be hidden since its parent was unhidden"); // The key here is that we did *not* receive any notifications about leaving the pool since it remained in the pool, // even though its parent was unhidden and it was hidden. } - (void)testThatComponentThatInjectsAnIntermediateViewNotControlledByComponentsDoesNotBreakViewReuseForItsSubviews { UIView *rootView = [[UIView alloc] init]; CKComponentLifecycleTestHelper *componentLifecycleTestController = [[CKComponentLifecycleTestHelper alloc] initWithComponentProvider:componentProvider sizeRangeProvider:nil]; [componentLifecycleTestController updateWithState:[componentLifecycleTestController prepareForUpdateWithModel:@NO constrainedSize:{{0,0}, {100, 100}} context:nil]]; [componentLifecycleTestController attachToView:rootView]; // Find the reuse aware view CKReuseAwareView *reuseAwareView = [[[[[[rootView subviews] firstObject] subviews] firstObject] subviews] firstObject]; XCTAssertFalse(reuseAwareView.inReusePool, @"Shouldn't be in reuse pool now, it's just been mounted"); // Update to a totally different component so that the reuse aware view's parent should be hidden [componentLifecycleTestController updateWithState:[componentLifecycleTestController prepareForUpdateWithModel:@YES constrainedSize:{{0,0}, {100, 100}} context:nil]]; XCTAssertTrue(reuseAwareView.inReusePool, @"Should be in reuse pool as its parent is hidden by components"); } static UIView *reuseAwareViewFactory() { return [[CKReuseAwareView alloc] init]; } static CKComponent *componentProvider(id<NSObject> model, id<NSObject>context) { if ([(NSNumber *)model boolValue]) { return CK::ComponentBuilder() .viewClass([UIView class]) .width(50) .height(50) .build(); } else { return [CKViewInjectingComponent newWithComponent: CK::ComponentBuilder() .viewClass({ &reuseAwareViewFactory, ^(UIView *v){ ((CKReuseAwareView *)v).inReusePool = YES; }, ^(UIView *v){ ((CKReuseAwareView *)v).inReusePool = NO; } }) .build()]; } } @end @interface CKInjectingView : UIView @property (nonatomic, strong, readonly) UIView *injectedView; @end @implementation CKInjectingView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { _injectedView = [[UIView alloc] initWithFrame:CGRectZero]; [self addSubview:_injectedView]; } return self; } - (void)layoutSubviews { [super layoutSubviews]; [_injectedView setFrame:{CGPointZero, [self bounds].size}]; } @end @implementation CKViewInjectingComponent + (instancetype)newWithComponent:(CKComponent *)component { return [super newWithView:{[CKInjectingView class]} component:component]; } - (CK::Component::MountResult)mountInContext:(const CK::Component::MountContext &)context layout:(const RCLayout &)layout supercomponent:(CKComponent *)supercomponent { const auto result = [super mountInContext:context layout:layout supercomponent:supercomponent]; CKInjectingView *injectingView = (CKInjectingView *)result.contextForChildren.viewManager->view; return { .mountChildren = YES, .contextForChildren = result.contextForChildren.childContextForSubview(injectingView.injectedView, NO), }; } @end @implementation CKReuseAwareView @end