ComponentKitTests/CKActionTests.mm (660 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/CKTestActionComponent.h> #import <ComponentKit/CKBuildComponent.h> #import <ComponentKit/CKAction.h> #import <ComponentKit/CKCompositeComponent.h> #import <ComponentKit/CKComponentSubclass.h> #import <ComponentKit/CKComponentInternal.h> #import <ComponentKit/CKComponentLayout.h> #import <ComponentKit/CKComponentScopeRoot.h> #import <ComponentKit/CKComponentScopeRootFactory.h> #import <ComponentKit/CKComponentController.h> #import <ComponentKit/CKThreadLocalComponentScope.h> #import "CKComponentTestCase.h" @interface CKComponentActionTestAssertionHandler : NSAssertionHandler @end @implementation CKComponentActionTestAssertionHandler - (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ... {} @end @interface CKTestScopeActionComponent : CKComponent + (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block; + (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block useComponentAsTarget:(BOOL)useComponentAsTarget; - (void)triggerAction:(id)context; @end @implementation CKTestScopeActionComponent { CKAction<id> _action; void (^_block)(CKComponent *, id); } + (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block { return [self newWithBlock:block useComponentAsTarget:NO]; } + (instancetype)newWithBlock:(void (^)(CKComponent *, id))block useComponentAsTarget:(BOOL)useComponentAsTarget { CKComponentScope scope(self); CKTestScopeActionComponent *c = [super new]; if (c) { if (useComponentAsTarget) { c->_action = {c, @selector(actionMethod:context:)}; } else { c->_action = {scope, @selector(actionMethod:context:)}; } c->_block = block; } return c; } - (void)actionMethod:(CKComponent *)sender context:(id)context { _block(sender, context); } - (void)triggerAction:(id)context { _action.send(self, context); } @end @interface CKTestControllerScopeActionComponent : CKComponent + (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block; + (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block useComponentAsTarget:(BOOL)useComponentAsTarget; - (void(^)(CKComponent *sender, id context))block; - (void)triggerAction:(id)context; @end @interface CKTestControllerScopeActionComponentController : CKComponentController<CKTestControllerScopeActionComponent *> @end @implementation CKTestControllerScopeActionComponent { CKAction<id> _action; void (^_block)(CKComponent *, id); } + (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block { return [self newWithBlock:block useComponentAsTarget:NO]; } + (instancetype)newWithBlock:(void (^)(CKComponent *, id))block useComponentAsTarget:(BOOL)useComponentAsTarget { CKComponentScope scope(self); CKTestControllerScopeActionComponent *c = [super new]; if (c) { if (useComponentAsTarget) { c->_action = {c, @selector(actionMethod:context:)}; } else { c->_action = {scope, @selector(actionMethod:context:)}; } c->_block = block; } return c; } + (Class<CKComponentControllerProtocol>)controllerClass { return [CKTestControllerScopeActionComponentController class]; } - (void (^)(CKComponent *, id))block { return _block; } - (void)triggerAction:(id)context { _action.send(self, context); } @end @interface CKTestObjectTarget : NSObject - (void)someMethod; @property (nonatomic, assign, readonly) BOOL calledSomeMethod; @end @implementation CKTestObjectTarget - (void)someMethod { _calledSomeMethod = YES; } @end @implementation CKTestControllerScopeActionComponentController - (void)actionMethod:(CKComponent *)sender context:(id)context { self.component.block(sender, context); } @end @interface CKActionTests : CKComponentTestCase @end @implementation CKActionTests - (void)testSendActionIncludesSenderComponent { __block CKComponent *actionSender = nil; CKComponent *innerComponent = [CKComponent new]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ actionSender = sender; } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); CKActionSend(@selector(testAction:context:), innerComponent, nil); XCTAssert(actionSender == innerComponent, @"Sender should be inner component"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } - (void)testSendActionIncludesContext { __block id actionContext = nil; CKComponent *innerComponent = [CKComponent new]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ actionContext = context; } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); id context = @"context"; CKActionSend(@selector(testAction:context:), innerComponent, context); XCTAssert(actionContext == context, @"Context should match what was passed to CKActionSend"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } - (void)testSendActionIncludesMultipleContextObjects { __block id actionContext = nil; __block id actionContext2 = nil; CKComponent *innerComponent = [CKComponent new]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { actionContext = obj1; actionContext2 = obj2; } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); id context = @"context"; id context2 = @"context2"; CKAction<id, id> action = { @selector(testAction2:context1:context2:) }; action.send(innerComponent, context, context2); XCTAssert(actionContext == context && actionContext2 == context2, @"Contexts should match what was passed to CKActionSend"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } - (void)testSendActionIncludingPrimitiveValue { __block int actionInteger = 0; CKComponent *innerComponent = [CKComponent new]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); } primitiveArgumentBlock:^(CKComponent *sender, int value) { actionInteger = value; } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); int integer = 1337; CKAction<int> action = { @selector(testPrimitive:integer:) }; action.send(innerComponent, integer); XCTAssert(actionInteger == integer, @"Contexts should match what was passed to CKActionSend"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } - (void)testActionWithCppArgs { __block std::vector<std::string> actionVec; CKComponent *innerComponent = [CKComponent new]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithCppArgumentBlock:^(CKComponent *sender, std::vector<std::string> vec) { actionVec = vec; } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); std::vector<std::string> cppThing = {"hummus", "chips", "salad"}; CKAction<const std::vector<std::string> &> action = { @selector(testCppArgumentAction:vector:) }; action.send(innerComponent, cppThing); XCTAssert(actionVec == cppThing, @"Contexts should match what was passed to CKActionSend"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } - (void)testSendActionWithNoArguments { __block BOOL calledNoArgumentBlock = NO; CKComponent *innerComponent = [CKComponent new]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { calledNoArgumentBlock = YES; } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); CKAction<> action = { @selector(testNoArgumentAction:) }; action.send(innerComponent); XCTAssert(calledNoArgumentBlock, @"Contexts should match what was passed to CKActionSend"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } - (void)testSendActionWithNoArgumentsWithAnActionThatExpectsObjectArguments { __block BOOL calledNoArgumentBlock = NO; CKComponent *innerComponent = [CKComponent new]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { calledNoArgumentBlock = YES; } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); CKAction<id> action = { @selector(testNoArgumentAction:) }; action.send(innerComponent, @"hello"); XCTAssert(calledNoArgumentBlock, @"Contexts should match what was passed to CKActionSend"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } static CKAction<> createDemotedWithReference(void (^callback)(CKComponent*, int), int value) { int& ref = value; auto action = CKAction<int>::actionFromBlock(callback); auto demoted = CKAction<>::demotedFrom(action, ref); return demoted; } - (void)testSendActionWithObjectArgumentsWithDemotedActionWithoutArguments { __block id actionContext = nil; __block id actionContext2 = nil; CKComponent *innerComponent = [CKComponent new]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { actionContext = obj1; actionContext2 = obj2; } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); id context = @"hello"; id context2 = @"morty"; CKAction<id, id> action = { @selector(testAction2:context1:context2:) }; CKAction<> demotedAction = CKAction<>::demotedFrom(action, context, context2); demotedAction.send(innerComponent); __block int value; int expectedValue = 5; createDemotedWithReference(^(CKComponent *sender, int b) { value = b; }, expectedValue).send(innerComponent); XCTAssert(actionContext == context && actionContext2 == context2 && value == expectedValue, @"Contexts should match what was passed to CKActionSend"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } - (void)testSendActionWithObjectArgumentsWithPromotedActionWithObjectArguments { __block id actionContext = nil; __block id actionContext2 = nil; CKComponent *innerComponent = [CKComponent new]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { actionContext = obj1; actionContext2 = obj2; } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); id context = @"hello"; id context2 = @"morty"; CKAction<id, id> action = { @selector(testAction2:context1:context2:) }; CKAction<id, id, id> promotedAction = CKAction<id, id>::promotedFrom<id>(action); promotedAction.send(innerComponent, context, context2, @"rick"); XCTAssert(actionContext == context && actionContext2 == context2, @"Contexts should match what was passed to CKActionSend"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } - (void)testSendActionStartingAtSenderNextResponderReachesParentComponent { __block BOOL outerReceivedTestAction = NO; __block BOOL innerReceivedTestAction = NO; CKTestActionComponent *innerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ innerReceivedTestAction = YES; } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:[CKComponent new]]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ outerReceivedTestAction = YES; } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); CKActionSend(@selector(testAction:context:), innerComponent, nullptr); XCTAssertTrue(outerReceivedTestAction, @"Outer component should have received action sent by inner component"); XCTAssertFalse(innerReceivedTestAction, @"Inner component should not have received action sent from it"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } - (void)testSendActionStartingAtSenderDoesNotReachParentComponent { __block BOOL outerReceivedTestAction = NO; __block BOOL innerReceivedTestAction = NO; CKTestActionComponent *innerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ innerReceivedTestAction = YES; } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:[CKComponent new]]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ outerReceivedTestAction = YES; } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:innerComponent]; // Must be mounted to send actions: UIView *rootView = [UIView new]; NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil); CKActionSend(@selector(testAction:context:), innerComponent, nil, CKActionSendBehaviorStartAtSender); XCTAssertFalse(outerReceivedTestAction, @"Outer component should not have received action since inner component did"); XCTAssertTrue(innerReceivedTestAction, @"Inner component should have received action"); [mountedComponents makeObjectsPerformSelector:@selector(unmount)]; } - (void)testCombineTwoActionsWithNoArguments { __block int action1CallCount = 0; __block int action2CallCount = 0; CKAction<> action1 = CKAction<>::actionFromBlock(^(CKComponent *) { XCTAssert(action1CallCount == 0, @"No actions should have been called yet"); XCTAssert(action2CallCount == 0, @"No actions should have been called yet"); action1CallCount++; }); CKAction<> action2 = CKAction<>::actionFromBlock(^(CKComponent *) { XCTAssert(action1CallCount == 1, @"First action should be called before the second action"); XCTAssert(action2CallCount == 0, @"Second action shouldn't have been called yet"); action2CallCount++; }); CKAction<>::combine(action1, action2).send([CKComponent new]); XCTAssert(action1CallCount == 1, @"First action should have been called once"); XCTAssert(action2CallCount == 1, @"Second action should habe been called once"); } - (void)testCombineThreeActionsWithNoArguments { __block int action1CallCount = 0; __block int action2CallCount = 0; __block int action3CallCount = 0; CKAction<> action1 = CKAction<>::actionFromBlock(^(CKComponent *) { XCTAssert(action1CallCount == 0, @"No actions should have been called yet"); XCTAssert(action2CallCount == 0, @"No actions should have been called yet"); XCTAssert(action3CallCount == 0, @"No actions should have been called yet"); action1CallCount++; }); CKAction<> action2 = CKAction<>::actionFromBlock(^(CKComponent *) { XCTAssert(action1CallCount == 1, @"First action should be called before the second action"); XCTAssert(action2CallCount == 0, @"Second action shouldn't have been called yet"); XCTAssert(action3CallCount == 0, @"Third action shouldn't have been called yet"); action2CallCount++; }); CKAction<> action3 = CKAction<>::actionFromBlock(^(CKComponent *) { XCTAssert(action1CallCount == 1, @"First action should be called before the second action"); XCTAssert(action2CallCount == 1, @"Second action should be called before the third action"); XCTAssert(action3CallCount == 0, @"Third action shouldn't have been called yet"); action3CallCount++; }); CKAction<>::combine(action1, action2, action3).send([CKComponent new]); XCTAssert(action1CallCount == 1, @"First action should have been called once"); XCTAssert(action2CallCount == 1, @"Second action should have been called once"); XCTAssert(action3CallCount == 1, @"Third action should have been called once"); } - (void)testTargetSelectorActionCallsOnTargetWithoutMounting { __block BOOL calledBlock = NO; CKComponent *innerComponent = [CKComponent new]; CKTestActionComponent *outerComponent = [CKTestActionComponent newWithSingleArgumentBlock:^(CKComponent *sender, id context){ calledBlock = YES; } secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); } primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); } noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); } component:innerComponent]; CKAction<id> action { outerComponent, @selector(testAction:context:) }; action.send(innerComponent, CKActionSendBehaviorStartAtSender, @"hello"); XCTAssertTrue(calledBlock, @"Outer component should have received the action, even though the components are not mounted."); } - (void)testScopeActionCallsMethodOnScopedComponent { __block BOOL calledAction = NO; // We have to use build component here to ensure the scopes are properly configured. CKTestScopeActionComponent *component = (CKTestScopeActionComponent *)CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{ return [CKTestScopeActionComponent newWithBlock:^(CKComponent *sender, id context) { calledAction = YES; }]; }).component; [component triggerAction:nil]; XCTAssertTrue(calledAction, @"Should have called the action on the test component"); } - (void)testComponentAsTargetActionCallsMethodOnComponent { __block BOOL calledAction = NO; // We have to use build component here to ensure the scopes are properly configured. CKTestScopeActionComponent *component = (CKTestScopeActionComponent *)CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{ return [CKTestScopeActionComponent newWithBlock:^(CKComponent *sender, id context) { calledAction = YES; } useComponentAsTarget:YES]; }).component; [component triggerAction:nil]; XCTAssertTrue(calledAction, @"Should have called the action on the test component"); } - (void)testScopeActionCallsMethodOnScopedComponentWithCorrectContext { __block id actionContext = nil; // We have to use build component here to ensure the scopes are properly configured. CKTestScopeActionComponent *component = (CKTestScopeActionComponent *)CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{ return [CKTestScopeActionComponent newWithBlock:^(CKComponent *sender, id context) { actionContext = context; }]; }).component; id context = @"hello"; [component triggerAction:context]; XCTAssertTrue(actionContext == context, @"Context should have been passed to scope component action call"); } - (void)testScopeActionCallsMethodOnScopedComponentControllerIfNotImplementedOnComponent { __block BOOL calledAction = NO; // We have to use build component here to ensure the scopes are properly configured. CKTestControllerScopeActionComponent *component = (CKTestControllerScopeActionComponent *)CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{ return [CKTestControllerScopeActionComponent newWithBlock:^(CKComponent *sender, id context) { calledAction = YES; }]; }).component; [component triggerAction:nil]; XCTAssertTrue(calledAction, @"Should have called the action on the test component"); } - (void)testComponentAsTargetActionCallsMethodOnComponentControllerIfNotImplementedOnComponent { __block BOOL calledAction = NO; // We have to use build component here to ensure the scopes are properly configured. CKTestControllerScopeActionComponent *component = (CKTestControllerScopeActionComponent *)CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{ return [CKTestControllerScopeActionComponent newWithBlock:^(CKComponent *sender, id context) { calledAction = YES; } useComponentAsTarget:YES]; }).component; [component triggerAction:nil]; XCTAssertTrue(calledAction, @"Should have called the action on the test component"); } - (void)testTargetSelectorActionCallsOnNormalNSObject { CKTestObjectTarget *target = [CKTestObjectTarget new]; CKAction<> action = {target, @selector(someMethod)}; action.send([CKComponent new]); XCTAssertTrue(target.calledSomeMethod, @"Should have called the method on target"); } - (void)testImpIsNilWhenSelectorIsNil { XCTAssert(!CKActionFind(nil, nil).imp); } - (void)testImpIsNilWhenTargetIsNil { XCTAssert(!CKActionFind(@selector(triggerAction:), nil).imp); } - (void)testResponderIsNilWhenSelectorIsNil { XCTAssertNil(CKActionFind(nil, nil).responder); } - (void)testResponderIsNilWhenTargetIsNil { XCTAssertNil(CKActionFind(@selector(triggerAction:), nil).responder); } - (void)testBlockActionFires { __block BOOL firedAction = NO; CKAction<> action = CKAction<>::actionFromBlock(^(CKComponent *) { firedAction = YES; }); action.send([CKComponent new]); XCTAssertTrue(firedAction); } - (void)testBlockActionFiresAndDeliversComponentAsSender { __block BOOL equalComponents = NO; CKComponent *c = [CKComponent new]; CKAction<> action = CKAction<>::actionFromBlock(^(CKComponent *passedComponent) { equalComponents = (passedComponent == c); }); action.send(c); XCTAssertTrue(equalComponents); } - (void)testBlockActionFiresAndDeliversAdditionalParameterAsArgument { __block BOOL equalArguments = NO; NSObject *arg = [NSObject new]; CKAction<NSObject *> action = CKAction<NSObject *>::actionFromBlock(^(CKComponent *c, NSObject *passedArgument) { equalArguments = (passedArgument == arg); }); action.send([CKComponent new], arg); XCTAssertTrue(equalArguments); } - (void)testThatScopeActionWithSameSelectorHaveUniqueIdentifiers { CKThreadLocalComponentScope threadScope(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}); CKComponentScope scope([CKTestScopeActionComponent class], @"moose"); const CKAction<> action1 = {scope, @selector(triggerAction:)}; CKComponentScope scope2([CKTestScopeActionComponent class], @"cat"); const CKAction<> action2 = {scope2, @selector(triggerAction:)}; XCTAssertNotEqual(action1.identifier(), action2.identifier()); } - (void)testThatBlockActionsWithDistinctBlocksHaveUniqueIdentifiers { const CKAction<> action1 = CKAction<>::actionFromBlock(^(CKComponent *sender){ exit(1); }); const CKAction<> action2 = CKAction<>::actionFromBlock(^(CKComponent *sender){ exit(2); }); XCTAssertNotEqual(action1.identifier(), action2.identifier()); } #pragma mark - Action Params Validation - (BOOL)checkSelector:(SEL)sel typeEncodings:(const std::vector<const char *> &)typeEncodings { Method method = class_getInstanceMethod([self class], sel); return checkMethodSignatureAgainstTypeEncodings(sel, method, typeEncodings); } - (void)testActionNoParamValidation { const SEL selector = @selector(triggerActionWithComponent:); std::vector<const char *> encodings; CKActionTypeVectorBuild(encodings, CKActionTypelist<>{}); XCTAssertTrue(([self checkSelector:selector typeEncodings:encodings])); } - (void)triggerActionWithComponent:(id)sender {} - (void)testActionPrimitiveParamValidation { const SEL selector = @selector(triggerActionWithComponent:value:); std::vector<const char *> encodings; CKActionTypeVectorBuild(encodings, CKActionTypelist<int>{}); XCTAssertTrue(([self checkSelector:selector typeEncodings:encodings])); } - (void)triggerActionWithComponent:(id)sender value:(int)val {} - (void)testActionObjectAndPrimitiveParamValidation { const SEL selector = @selector(triggerActionWithComponent:value:value:); std::vector<const char *> encodings; CKActionTypeVectorBuild(encodings, CKActionTypelist<NSString *, char>{}); XCTAssertTrue(([self checkSelector:selector typeEncodings:encodings])); } - (void)triggerActionWithComponent:(id)sender value:(NSString *)obj value:(char)val {} - (void)testActionCppParamsValidation { const SEL selector = @selector(triggerActionWithComponent:vector:constVector:constValVector:vectorRef:vectorRval:); std::vector<const char *> encodings; CKActionTypeVectorBuild(encodings, CKActionTypelist< std::vector<int>, const std::vector<int>, std::vector<const int>, std::vector<int> &, std::vector<int> && >{}); XCTAssertTrue(([self checkSelector:selector typeEncodings:encodings])); } - (void)triggerActionWithComponent:(id)sender vector:(std::vector<int>)val constVector:(const std::vector<int>)conVal constValVector:(std::vector<const int>)conValVec vectorRef:(std::vector<int> &)vecRef vectorRval:(std::vector<int> &&)vecRval {} - (void)testActionParamsFailedValidation { // We need to set an assertion handler as `checkMethodSignatureAgainstTypeEncodings` throws `RCCFailAssert` in case it fails. auto const assertionHandler = [CKComponentActionTestAssertionHandler new]; [[[NSThread currentThread] threadDictionary] setValue:assertionHandler forKey:NSAssertionHandlerKey]; const SEL selector = @selector(triggerActionWithComponent:vector:object:primitive:); std::vector<const char *> encodings; // wrong c++ type CKActionTypeVectorBuild(encodings, CKActionTypelist<std::vector<NSURL *>, NSObject *, BOOL>{}); XCTAssertFalse(([self checkSelector:selector typeEncodings:encodings])); // wrong object CKActionTypeVectorBuild(encodings, CKActionTypelist<std::vector<int>, NSInteger, BOOL>{}); XCTAssertFalse(([self checkSelector:selector typeEncodings:encodings])); // wrong primitive CKActionTypeVectorBuild(encodings, CKActionTypelist<std::vector<int>, NSObject *, char >{}); XCTAssertFalse(([self checkSelector:selector typeEncodings:encodings])); } - (void)triggerActionWithComponent:(id)sender vector:(std::vector<int>)val object:(NSObject *)conVal primitive:(BOOL)prim {} #pragma mark - Equality. - (void)testRawSelectorEquality { const SEL selector = @selector(triggerAction:); const CKUntypedComponentAction action1 = {selector}; const CKUntypedComponentAction action2 = {selector}; XCTAssertTrue(action1 == action2); const CKUntypedComponentAction unequalAction = {@selector(stringWithFormat:)}; XCTAssertFalse(action1 == unequalAction); } - (void)testTargetSelectorActionEquality { NSMutableArray *const target = [NSMutableArray new]; const SEL selector = @selector(removeLastObject); const CKAction<> action1 = {target, selector}; const CKAction<> action2 = {target, selector}; XCTAssertTrue(action1 == action2); const CKAction<> actionWithUnequalTarget = {[NSMutableArray new], selector}; XCTAssertFalse(action1 == actionWithUnequalTarget); const CKAction<> actionWithUnequalSelector = {target, @selector(removeAllObjects)}; XCTAssertFalse(action1 == actionWithUnequalSelector); } - (void)testBlockActionEquality { void (^block)(CKComponent *c, NSObject *passedArgument) {}; const CKAction<NSObject *> action = CKAction<NSObject *>::actionFromBlock(block); XCTAssertTrue(action == CKAction<NSObject *>::actionFromBlock(block)); XCTAssertFalse(action == CKAction<NSObject *>::actionFromBlock(^(CKComponent *, NSObject *__strong) {})); } - (void)testScopedActionEquality { CKThreadLocalComponentScope threadScope(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}); const SEL selector = @selector(triggerAction:); CKComponentScope scope([CKTestScopeActionComponent class], @"Marty McFly"); const CKAction<> action1 = {scope, selector}; const CKAction<> action2 = {scope, selector}; XCTAssertTrue(action1 == action2); CKComponentScope scope2([CKTestScopeActionComponent class], @"Biff Tannon"); const CKAction<> unequalAction = {scope2, selector}; XCTAssertFalse(action1 == unequalAction); } @end