| // | |
| // NSObjectRACSelectorSignalSpec.m | |
| // ReactiveCocoa | |
| // | |
| // Created by Josh Abernathy on 3/18/13. | |
| // Copyright (c) 2013 GitHub, Inc. All rights reserved. | |
| // | |
| #import "RACTestObject.h" | |
| #import "RACSubclassObject.h" | |
| #import "NSObject+RACDeallocating.h" | |
| #import "NSObject+RACPropertySubscribing.h" | |
| #import "NSObject+RACSelectorSignal.h" | |
| #import "RACCompoundDisposable.h" | |
| #import "RACDisposable.h" | |
| #import "RACMulticastConnection.h" | |
| #import "RACSignal+Operations.h" | |
| #import "RACSignal.h" | |
| #import "RACTuple.h" | |
| @protocol TestProtocol | |
| @required | |
| - (BOOL)requiredMethod:(NSUInteger)number; | |
| - (void)lifeIsGood:(id)sender; | |
| @optional | |
| - (NSUInteger)optionalMethodWithObject:(id)object flag:(BOOL)flag; | |
| - (id)objectValue; | |
| @end | |
| SpecBegin(NSObjectRACSelectorSignal) | |
| describe(@"RACTestObject", ^{ | |
| it(@"should send the argument for each invocation", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(lifeIsGood:)] subscribeNext:^(RACTuple *x) { | |
| value = x.first; | |
| }]; | |
| [object lifeIsGood:@42]; | |
| expect(value).to.equal(@42); | |
| }); | |
| it(@"should send completed on deallocation", ^{ | |
| __block BOOL completed = NO; | |
| __block BOOL deallocated = NO; | |
| @autoreleasepool { | |
| RACTestObject *object __attribute__((objc_precise_lifetime)) = [[RACTestObject alloc] init]; | |
| [object.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ | |
| deallocated = YES; | |
| }]]; | |
| [[object rac_signalForSelector:@selector(lifeIsGood:)] subscribeCompleted:^{ | |
| completed = YES; | |
| }]; | |
| expect(deallocated).to.beFalsy(); | |
| expect(completed).to.beFalsy(); | |
| } | |
| expect(deallocated).to.beTruthy(); | |
| expect(completed).to.beTruthy(); | |
| }); | |
| it(@"should send for a zero-argument method", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| __block RACTuple *value; | |
| [[object rac_signalForSelector:@selector(objectValue)] subscribeNext:^(RACTuple *x) { | |
| value = x; | |
| }]; | |
| [object objectValue]; | |
| expect(value).to.equal([RACTuple tupleWithObjectsFromArray:@[]]); | |
| }); | |
| it(@"should send the argument for each invocation to the instance's own signal", ^{ | |
| RACTestObject *object1 = [[RACTestObject alloc] init]; | |
| __block id value1; | |
| [[object1 rac_signalForSelector:@selector(lifeIsGood:)] subscribeNext:^(RACTuple *x) { | |
| value1 = x.first; | |
| }]; | |
| RACTestObject *object2 = [[RACTestObject alloc] init]; | |
| __block id value2; | |
| [[object2 rac_signalForSelector:@selector(lifeIsGood:)] subscribeNext:^(RACTuple *x) { | |
| value2 = x.first; | |
| }]; | |
| [object1 lifeIsGood:@42]; | |
| [object2 lifeIsGood:@"Carpe diem"]; | |
| expect(value1).to.equal(@42); | |
| expect(value2).to.equal(@"Carpe diem"); | |
| }); | |
| it(@"should send multiple arguments for each invocation", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| __block id value1; | |
| __block id value2; | |
| [[object rac_signalForSelector:@selector(combineObjectValue:andSecondObjectValue:)] subscribeNext:^(RACTuple *x) { | |
| value1 = x.first; | |
| value2 = x.second; | |
| }]; | |
| expect([object combineObjectValue:@42 andSecondObjectValue:@"foo"]).to.equal(@"42: foo"); | |
| expect(value1).to.equal(@42); | |
| expect(value2).to.equal(@"foo"); | |
| }); | |
| it(@"should send arguments for invocation of non-existant methods", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| __block id key; | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(setObject:forKey:)] subscribeNext:^(RACTuple *x) { | |
| value = x.first; | |
| key = x.second; | |
| }]; | |
| [object performSelector:@selector(setObject:forKey:) withObject:@YES withObject:@"Winner"]; | |
| expect(value).to.equal(@YES); | |
| expect(key).to.equal(@"Winner"); | |
| }); | |
| it(@"should send arguments for invocation and invoke the original method on previously KVO'd receiver", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| [[RACObserve(object, objectValue) publish] connect]; | |
| __block id key; | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(setObjectValue:andSecondObjectValue:)] subscribeNext:^(RACTuple *x) { | |
| value = x.first; | |
| key = x.second; | |
| }]; | |
| [object setObjectValue:@YES andSecondObjectValue:@"Winner"]; | |
| expect(object.hasInvokedSetObjectValueAndSecondObjectValue).to.beTruthy(); | |
| expect(object.objectValue).to.equal(@YES); | |
| expect(object.secondObjectValue).to.equal(@"Winner"); | |
| expect(value).to.equal(@YES); | |
| expect(key).to.equal(@"Winner"); | |
| }); | |
| it(@"should send arguments for invocation and invoke the original method when receiver is subsequently KVO'd", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| __block id key; | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(setObjectValue:andSecondObjectValue:)] subscribeNext:^(RACTuple *x) { | |
| value = x.first; | |
| key = x.second; | |
| }]; | |
| [[RACObserve(object, objectValue) publish] connect]; | |
| [object setObjectValue:@YES andSecondObjectValue:@"Winner"]; | |
| expect(object.hasInvokedSetObjectValueAndSecondObjectValue).to.beTruthy(); | |
| expect(object.objectValue).to.equal(@YES); | |
| expect(object.secondObjectValue).to.equal(@"Winner"); | |
| expect(value).to.equal(@YES); | |
| expect(key).to.equal(@"Winner"); | |
| }); | |
| it(@"should properly implement -respondsToSelector: when called on KVO'd receiver", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| // First, setup KVO on `object`, which gives us the desired side-effect | |
| // of `object` taking on a KVO-custom subclass. | |
| [[RACObserve(object, objectValue) publish] connect]; | |
| SEL selector = NSSelectorFromString(@"anyOldSelector:"); | |
| // With the KVO subclass in place, call -rac_signalForSelector: to | |
| // implement -anyOldSelector: directly on the KVO subclass. | |
| [object rac_signalForSelector:selector]; | |
| expect([object respondsToSelector:selector]).to.beTruthy(); | |
| }); | |
| it(@"should properly implement -respondsToSelector: for optional method from a protocol", ^{ | |
| // Selector for the targeted optional method from a protocol. | |
| SEL selector = @selector(optionalProtocolMethodWithObjectValue:); | |
| RACTestObject *object1 = [[RACTestObject alloc] init]; | |
| // Method implementation of the selector is added to its swizzled class. | |
| [object1 rac_signalForSelector:selector fromProtocol:@protocol(RACTestProtocol)]; | |
| expect([object1 respondsToSelector:selector]).to.beTruthy(); | |
| RACTestObject *object2 = [[RACTestObject alloc] init]; | |
| // Call -rac_signalForSelector: to swizzle this instance's class, | |
| // method implementations of -respondsToSelector: and | |
| // -forwardInvocation:. | |
| [object2 rac_signalForSelector:@selector(lifeIsGood:)]; | |
| // This instance should not respond to the selector because of not | |
| // calling -rac_signalForSelector: with the selector. | |
| expect([object2 respondsToSelector:selector]).to.beFalsy(); | |
| }); | |
| it(@"should send non-object arguments", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(setIntegerValue:)] subscribeNext:^(RACTuple *x) { | |
| value = x.first; | |
| }]; | |
| object.integerValue = 42; | |
| expect(value).to.equal(@42); | |
| }); | |
| it(@"should send on signal after the original method is invoked", ^{ | |
| RACTestObject *object = [[RACTestObject alloc] init]; | |
| __block BOOL invokedMethodBefore = NO; | |
| [[object rac_signalForSelector:@selector(setObjectValue:andSecondObjectValue:)] subscribeNext:^(RACTuple *x) { | |
| invokedMethodBefore = object.hasInvokedSetObjectValueAndSecondObjectValue; | |
| }]; | |
| [object setObjectValue:@YES andSecondObjectValue:@"Winner"]; | |
| expect(invokedMethodBefore).to.beTruthy(); | |
| }); | |
| }); | |
| it(@"should swizzle an NSObject method", ^{ | |
| NSObject *object = [[NSObject alloc] init]; | |
| __block RACTuple *value; | |
| [[object rac_signalForSelector:@selector(description)] subscribeNext:^(RACTuple *x) { | |
| value = x; | |
| }]; | |
| expect([object description]).notTo.beNil(); | |
| expect(value).to.equal([RACTuple tupleWithObjectsFromArray:@[]]); | |
| }); | |
| describe(@"a class that already overrides -forwardInvocation:", ^{ | |
| it(@"should invoke the superclass' implementation", ^{ | |
| RACSubclassObject *object = [[RACSubclassObject alloc] init]; | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(lifeIsGood:)] subscribeNext:^(RACTuple *x) { | |
| value = x.first; | |
| }]; | |
| [object lifeIsGood:@42]; | |
| expect(value).to.equal(@42); | |
| expect(object.forwardedSelector).to.beNil(); | |
| [object performSelector:@selector(allObjects)]; | |
| expect(value).to.equal(@42); | |
| expect(object.forwardedSelector).to.equal(@selector(allObjects)); | |
| }); | |
| it(@"should not infinite recurse when KVO'd after RAC swizzled", ^{ | |
| RACSubclassObject *object = [[RACSubclassObject alloc] init]; | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(lifeIsGood:)] subscribeNext:^(RACTuple *x) { | |
| value = x.first; | |
| }]; | |
| [[RACObserve(object, objectValue) publish] connect]; | |
| [object lifeIsGood:@42]; | |
| expect(value).to.equal(@42); | |
| expect(object.forwardedSelector).to.beNil(); | |
| [object performSelector:@selector(allObjects)]; | |
| expect(object.forwardedSelector).to.equal(@selector(allObjects)); | |
| }); | |
| }); | |
| describe(@"two classes in the same hierarchy", ^{ | |
| __block RACTestObject *superclassObj; | |
| __block RACTuple *superclassTuple; | |
| __block RACSubclassObject *subclassObj; | |
| __block RACTuple *subclassTuple; | |
| beforeEach(^{ | |
| superclassObj = [[RACTestObject alloc] init]; | |
| expect(superclassObj).notTo.beNil(); | |
| subclassObj = [[RACSubclassObject alloc] init]; | |
| expect(subclassObj).notTo.beNil(); | |
| }); | |
| it(@"should not collide", ^{ | |
| [[superclassObj rac_signalForSelector:@selector(combineObjectValue:andIntegerValue:)] subscribeNext:^(RACTuple *t) { | |
| superclassTuple = t; | |
| }]; | |
| [[subclassObj rac_signalForSelector:@selector(combineObjectValue:andIntegerValue:)] subscribeNext:^(RACTuple *t) { | |
| subclassTuple = t; | |
| }]; | |
| expect([superclassObj combineObjectValue:@"foo" andIntegerValue:42]).to.equal(@"foo: 42"); | |
| NSArray *expectedValues = @[ @"foo", @42 ]; | |
| expect(superclassTuple.allObjects).to.equal(expectedValues); | |
| expect([subclassObj combineObjectValue:@"foo" andIntegerValue:42]).to.equal(@"fooSUBCLASS: 42"); | |
| expectedValues = @[ @"foo", @42 ]; | |
| expect(subclassTuple.allObjects).to.equal(expectedValues); | |
| }); | |
| it(@"should not collide when the superclass is invoked asynchronously", ^{ | |
| [[superclassObj rac_signalForSelector:@selector(setObjectValue:andSecondObjectValue:)] subscribeNext:^(RACTuple *t) { | |
| superclassTuple = t; | |
| }]; | |
| [[subclassObj rac_signalForSelector:@selector(setObjectValue:andSecondObjectValue:)] subscribeNext:^(RACTuple *t) { | |
| subclassTuple = t; | |
| }]; | |
| [superclassObj setObjectValue:@"foo" andSecondObjectValue:@"42"]; | |
| expect(superclassObj.hasInvokedSetObjectValueAndSecondObjectValue).to.beTruthy(); | |
| NSArray *expectedValues = @[ @"foo", @"42" ]; | |
| expect(superclassTuple.allObjects).to.equal(expectedValues); | |
| [subclassObj setObjectValue:@"foo" andSecondObjectValue:@"42"]; | |
| expect(subclassObj.hasInvokedSetObjectValueAndSecondObjectValue).to.beFalsy(); | |
| expect(subclassObj.hasInvokedSetObjectValueAndSecondObjectValue).will.beTruthy(); | |
| expectedValues = @[ @"foo", @"42" ]; | |
| expect(subclassTuple.allObjects).to.equal(expectedValues); | |
| }); | |
| }); | |
| describe(@"-rac_signalForSelector:fromProtocol", ^{ | |
| __block RACTestObject<TestProtocol> *object; | |
| __block Protocol *protocol; | |
| beforeEach(^{ | |
| object = (id)[[RACTestObject alloc] init]; | |
| expect(object).notTo.beNil(); | |
| protocol = @protocol(TestProtocol); | |
| expect(protocol).notTo.beNil(); | |
| }); | |
| it(@"should not clobber a required method already implemented", ^{ | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(lifeIsGood:) fromProtocol:protocol] subscribeNext:^(RACTuple *x) { | |
| value = x.first; | |
| }]; | |
| [object lifeIsGood:@42]; | |
| expect(value).to.equal(@42); | |
| }); | |
| it(@"should not clobber an optional method already implemented", ^{ | |
| object.objectValue = @"foo"; | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(objectValue) fromProtocol:protocol] subscribeNext:^(RACTuple *x) { | |
| value = x; | |
| }]; | |
| expect([object objectValue]).to.equal(@"foo"); | |
| expect(value).to.equal([RACTuple tupleWithObjectsFromArray:@[]]); | |
| }); | |
| it(@"should inject a required method", ^{ | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(requiredMethod:) fromProtocol:protocol] subscribeNext:^(RACTuple *x) { | |
| value = x.first; | |
| }]; | |
| expect([object requiredMethod:42]).to.beFalsy(); | |
| expect(value).to.equal(42); | |
| }); | |
| it(@"should inject an optional method", ^{ | |
| __block id value; | |
| [[object rac_signalForSelector:@selector(optionalMethodWithObject:flag:) fromProtocol:protocol] subscribeNext:^(RACTuple *x) { | |
| value = x; | |
| }]; | |
| expect([object optionalMethodWithObject:@"foo" flag:YES]).to.equal(0); | |
| expect(value).to.equal(RACTuplePack(@"foo", @YES)); | |
| }); | |
| }); | |
| describe(@"class reporting", ^{ | |
| __block RACTestObject *object; | |
| __block Class originalClass; | |
| beforeEach(^{ | |
| object = [[RACTestObject alloc] init]; | |
| originalClass = object.class; | |
| }); | |
| it(@"should report the original class", ^{ | |
| [object rac_signalForSelector:@selector(lifeIsGood:)]; | |
| expect(object.class).to.beIdenticalTo(originalClass); | |
| }); | |
| it(@"should report the original class when it's KVO'd after dynamically subclassing", ^{ | |
| [object rac_signalForSelector:@selector(lifeIsGood:)]; | |
| [[RACObserve(object, objectValue) publish] connect]; | |
| expect(object.class).to.beIdenticalTo(originalClass); | |
| }); | |
| it(@"should report the original class when it's KVO'd before dynamically subclassing", ^{ | |
| [[RACObserve(object, objectValue) publish] connect]; | |
| [object rac_signalForSelector:@selector(lifeIsGood:)]; | |
| expect(object.class).to.beIdenticalTo(originalClass); | |
| }); | |
| }); | |
| SpecEnd |