ComponentKitTests/CKComponentViewAttributeTests.mm (281 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 <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import <ComponentKitTestHelpers/CKComponentLifecycleTestHelper.h>
#import <ComponentKit/CKComponent.h>
#import <ComponentKit/CKComponentSubclass.h>
#import "CKComponentTestCase.h"
@interface CKComponentViewAttributeTests : CKComponentTestCase
@end
@interface CKSetterCounterView : UIView
@property (nonatomic, copy) NSString *title;
@property (nonatomic, readonly) NSUInteger numberOfTimesSetTitleWasCalled;
@end
@interface CKHidingCounterView : UIView
@property (nonatomic, readonly) NSUInteger numberOfTimesViewWasHidden;
@end
@interface CKNSNumberView : UIControl
@property (nonatomic) char primitiveChar;
@property (nonatomic) int primitiveInt;
@property (nonatomic) short primitiveShort;
@property (nonatomic) int32_t primitiveInt32;
@property (nonatomic) long long primitiveInt64;
@property (nonatomic) unsigned char primitiveUChar;
@property (nonatomic) unsigned int primitiveUInt;
@property (nonatomic) unsigned short primitiveUShort;
@property (nonatomic) uint32_t primitiveUInt32;
@property (nonatomic) unsigned long long primitiveUInt64;
@property (nonatomic) double primitiveDouble;
@property (nonatomic) float primitiveFloat;
@end
@implementation CKComponentViewAttributeTests
- (void)testThatMountingViewWithNSValueAttributeActuallyAppliesAttributeToView
{
CKComponent *testComponent = CK::ComponentBuilder()
.viewClass([CKNSNumberView class])
.attribute(@selector(setSelected:), @YES)
.attribute(CKComponentViewAttribute::LayerAttribute(@selector(setOpacity:)), @(0.5))
.attribute(@selector(setTag:), @2)
.attribute(@selector(setPrimitiveChar:), @'D')
.attribute(@selector(setPrimitiveShort:), @(short(1)))
.attribute(@selector(setPrimitiveInt:), @14)
.attribute(@selector(setPrimitiveInt32:), @9L)
.attribute(@selector(setPrimitiveInt64:), @5LL)
.attribute(@selector(setPrimitiveUChar:), [NSNumber numberWithUnsignedChar:'L'])
.attribute(@selector(setPrimitiveUShort:), [NSNumber numberWithUnsignedShort:23])
.attribute(@selector(setPrimitiveUInt32:), @15UL)
.attribute(@selector(setPrimitiveUInt64:), @18ULL)
.attribute(@selector(setPrimitiveDouble:), @11.3)
.attribute(@selector(setPrimitiveFloat:), @21.1F)
.build();
CKComponentLifecycleTestHelper *componentLifecycleTestController = [[CKComponentLifecycleTestHelper alloc] initWithComponentProvider:nullptr
sizeRangeProvider:nil];
[componentLifecycleTestController updateWithState:{
.componentLayout = [testComponent layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
UIView *view = [UIView new];
[componentLifecycleTestController attachToView:view];
CKNSNumberView *c = [[view subviews] firstObject];
XCTAssertTrue([c isSelected], @"Expected selected to be applied to view");
XCTAssertTrue(c.layer.opacity == 0.5, @"Expected opacity to be applied to view's layer");
}
- (void)testThatMountingViewWithPrimitiveAttributeActuallyAppliesAttributeToView
{
CKComponent *testComponent = CK::ComponentBuilder()
.viewClass([CKNSNumberView class])
.attribute(@selector(setSelected:), YES)
.attribute(CKComponentViewAttribute::LayerAttribute(@selector(setOpacity:)), @0.5)
.attribute(@selector(setTag:), 2)
.attribute(@selector(setPrimitiveChar:), 'D')
.attribute(@selector(setPrimitiveShort:), short(1))
.attribute(@selector(setPrimitiveInt:), 14)
.attribute(@selector(setPrimitiveInt32:), 9L)
.attribute(@selector(setPrimitiveInt64:), 5LL)
.attribute(@selector(setPrimitiveUChar:), (unsigned char)('L'))
.attribute(@selector(setPrimitiveUShort:), (ushort)(23))
.attribute(@selector(setPrimitiveUInt32:), 15UL)
.attribute(@selector(setPrimitiveUInt64:), 18ULL)
.attribute(@selector(setPrimitiveDouble:), 11.3)
.attribute(@selector(setPrimitiveFloat:), 21.1F)
.build();
CKComponentLifecycleTestHelper *componentLifecycleTestController = [[CKComponentLifecycleTestHelper alloc] initWithComponentProvider:nullptr
sizeRangeProvider:nil];
[componentLifecycleTestController updateWithState:{
.componentLayout = [testComponent layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
UIView *container = [[UIView alloc] init];
[componentLifecycleTestController attachToView:container];
CKNSNumberView *c = [[container subviews] firstObject];
XCTAssertTrue([c isSelected], @"Expected selected to be applied to view");
XCTAssertTrue(c.layer.opacity == 0.5, @"Expected opacity to be applied to view's layer");
}
- (void)testThatRecyclingViewWithSameAttributeValueDoesNotReApplyAttributeToView
{
CKComponent *testComponent1 = CK::ComponentBuilder()
.viewClass([CKSetterCounterView class])
.attribute(@selector(setTitle:), @"Test")
.backgroundColor([UIColor blueColor])
.build();
CKComponentLifecycleTestHelper *componentLifecycleTestController1 = [[CKComponentLifecycleTestHelper alloc] initWithComponentProvider:nullptr
sizeRangeProvider:nil];
[componentLifecycleTestController1 updateWithState:{
.componentLayout = [testComponent1 layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
UIView *view = [UIView new];
[componentLifecycleTestController1 attachToView:view];
CKComponent *testComponent2 = CK::ComponentBuilder()
.viewClass([CKSetterCounterView class])
.attribute(@selector(setTitle:), @"Test")
.backgroundColor([UIColor redColor])
.build();
CKComponentLifecycleTestHelper *componentLifecycleTestController2 = [[CKComponentLifecycleTestHelper alloc] initWithComponentProvider:nullptr
sizeRangeProvider:nil];
[componentLifecycleTestController2 updateWithState:{
.componentLayout = [testComponent2 layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
[componentLifecycleTestController2 attachToView:view];
CKSetterCounterView *c = [[view subviews] firstObject];
XCTAssertEqual([c numberOfTimesSetTitleWasCalled], 1u, @"Expected setTitle: to called only once since it didn't change");
XCTAssertEqualObjects([c backgroundColor], [UIColor redColor], @"Expected background color to be updated by m2");
}
- (void)testThatRecyclingViewWithDistinctAttributeValueDoesNotHideAndReShowView
{
CKComponent *testComponent1 = CK::ComponentBuilder()
.viewClass([CKHidingCounterView class])
.backgroundColor([UIColor blueColor])
.build();
CKComponentLifecycleTestHelper *componentLifecycleTestController1 = [[CKComponentLifecycleTestHelper alloc] initWithComponentProvider:nullptr
sizeRangeProvider:nil];
[componentLifecycleTestController1 updateWithState:{
.componentLayout = [testComponent1 layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
UIView *view = [UIView new];
[componentLifecycleTestController1 attachToView:view];
CKComponent *testComponent2 = CK::ComponentBuilder()
.viewClass([CKHidingCounterView class])
.backgroundColor([UIColor redColor])
.build();
CKComponentLifecycleTestHelper *componentLifecycleTestController2 = [[CKComponentLifecycleTestHelper alloc] initWithComponentProvider:nullptr
sizeRangeProvider:nil];
[componentLifecycleTestController2 updateWithState:{
.componentLayout = [testComponent2 layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
[componentLifecycleTestController2 attachToView:view];
CKHidingCounterView *c = [[view subviews] firstObject];
XCTAssertEqual([c numberOfTimesViewWasHidden], 0u, @"Expected view never to be hidden if it didn't need to be");
}
- (void)testThatRecyclingViewWithAttributeRequiringUnapplicationCallsUnapplicator
{
NSMutableSet *appliedValues = [NSMutableSet set];
NSMutableSet *unappliedValues = [NSMutableSet set];
CKComponentViewAttribute attrWithUnapplicator("test", ^(id view, id val){
[appliedValues addObject:val];
}, ^(id view, id val){
[unappliedValues addObject:val];
});
CKComponent *testComponent1 = CK::ComponentBuilder()
.viewClass([UIView class])
.attribute(attrWithUnapplicator, @1)
.build();
CKComponentLifecycleTestHelper *componentLifecycleTestController = [[CKComponentLifecycleTestHelper alloc] initWithComponentProvider:nullptr
sizeRangeProvider:nil];
[componentLifecycleTestController updateWithState:{
.componentLayout = [testComponent1 layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
UIView *view = [UIView new];
[componentLifecycleTestController attachToView:view];
XCTAssertEqualObjects(appliedValues, [NSSet setWithObject:@1], @"Expected @1 to be applied");
XCTAssertEqualObjects(unappliedValues, [NSSet set], @"Expected nothing to be unapplied");
CKComponent *testComponent2 = CK::ComponentBuilder()
.viewClass([UIView class])
.attribute(attrWithUnapplicator, @2)
.build();
[componentLifecycleTestController updateWithState:{
.componentLayout = [testComponent2 layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
NSSet *expectedApplied = [NSSet setWithObjects:@1, @2, nil]; // stupid STAssert macros
XCTAssertEqualObjects(appliedValues, expectedApplied, @"Expected @1, @2 to be applied");
XCTAssertEqualObjects(unappliedValues, [NSSet setWithObject:@1], @"Expected @1 to be unapplied");
}
- (void)testThatRecyclingViewWithAttributeOfferingUpdaterInvokesUpdaterInsteadOfApplicator
{
__block std::vector<id> appliedValues;
__block std::vector<std::pair<id, id>> updatedValues;
CKComponentViewAttribute attrWithUpdater =
{
"test",
^(id view, id val){
appliedValues.push_back(val);
},
nil,
^(id view, id oldVal, id newVal){
updatedValues.push_back({oldVal, newVal});
}
};
CKComponent *testComponent1 = CK::ComponentBuilder()
.viewClass([UIView class])
.attribute(attrWithUpdater, @1)
.build();
CKComponentLifecycleTestHelper *componentLifecycleTestController = [[CKComponentLifecycleTestHelper alloc] initWithComponentProvider:nullptr
sizeRangeProvider:nil];
[componentLifecycleTestController updateWithState:{
.componentLayout = [testComponent1 layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
UIView *view = [UIView new];
[componentLifecycleTestController attachToView:view];
XCTAssertTrue(appliedValues.size() == 1 && [appliedValues[0] isEqual:@1], @"Expected @1 to be applied");
XCTAssertEqual(updatedValues.size(), (size_t)0, @"Expected no updates yet");
CKComponent *testComponent2 = CK::ComponentBuilder()
.viewClass([UIView class])
.attribute(attrWithUpdater, @2)
.build();
[componentLifecycleTestController updateWithState:{
.componentLayout = [testComponent2 layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
XCTAssertTrue(appliedValues.size() == 1, @"applicator should not have been called again");
XCTAssertTrue(updatedValues.size() == 1, @"updater should have been called");
std::pair<id, id> updateValue = updatedValues[0];
XCTAssertTrue([updateValue.first isEqual:@1] && [updateValue.second isEqual:@2],
@"updater should have been called with old and new values");
}
/**
Tests that if:
- If your attribute has BOTH an updater AND an unapplicator;
- The view is initially configured to show a component WITH the attribute;
- And the view is recycled to show a component WITHOUT the attribute;
- And then the view is recycled to show a component WITH the attribute again;
Then we should call the unapplicator followed by the applicator, not the updater.
*/
- (void)testThatRecyclingViewWithAttributeWithBothUpdaterAndUnapplicatorCallsUnapplicatorToRemoveThenApplicatorToReapply
{
NSMutableSet *appliedValues = [NSMutableSet set];
NSMutableSet *unappliedValues = [NSMutableSet set];
__block NSUInteger updateCount = 0;
CKComponentViewAttribute attr =
{
"test",
^(id view, id val){
[appliedValues addObject:val];
},
^(id view, id val) {
[unappliedValues addObject:val];
},
^(id view, id oldVal, id newVal){
updateCount++;
}
};
CKComponent *testComponent1 = CK::ComponentBuilder()
.viewClass([UIView class])
.attribute(attr, @1)
.build();
CKComponentLifecycleTestHelper *componentLifecycleTestController = [[CKComponentLifecycleTestHelper alloc] initWithComponentProvider:nullptr
sizeRangeProvider:nil];
[componentLifecycleTestController updateWithState:{
.componentLayout = [testComponent1 layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
UIView *container = [[UIView alloc] init];
[componentLifecycleTestController attachToView:container];
NSSet *expectedAppliedValues = [NSSet setWithObject:@1];
XCTAssertEqualObjects(appliedValues, expectedAppliedValues, @"Expected @1 to be applied");
XCTAssertEqualObjects(unappliedValues, [NSSet set], @"Nothing should be unapplied yet");
XCTAssertEqual(updateCount, 0u, @"Nothing should be updated");
CKComponent *testComponent2 = CK::ComponentBuilder()
.viewClass([UIView class])
.build();
[componentLifecycleTestController updateWithState:{
.componentLayout = [testComponent2 layoutThatFits:{{0, 0}, {10, 10}} parentSize:kCKComponentParentSizeUndefined]
}];
NSSet *expectedUnappliedValues = [NSSet setWithObject:@1];
XCTAssertEqualObjects(unappliedValues, expectedUnappliedValues, @"@1 should be unapplied");
XCTAssertEqual(updateCount, 0u, @"Nothing should be updated");
}
@end
@implementation CKSetterCounterView
- (void)setTitle:(NSString *)title
{
_numberOfTimesSetTitleWasCalled++;
_title = [title copy];
}
@end
@implementation CKHidingCounterView
- (void)setHidden:(BOOL)hidden
{
if (hidden) {
_numberOfTimesViewWasHidden++;
}
[super setHidden:hidden];
}
@end
@implementation CKNSNumberView
@end