// // NSObjectRACSelectorSignalSpec.m // ReactiveCocoa // // Created by Josh Abernathy on 3/18/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import #import #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 QuickSpecBegin(NSObjectRACSelectorSignalSpec) qck_describe(@"RACTestObject", ^{ qck_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)); }); qck_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()); }); qck_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; }]; (void)[object objectValue]; expect(value).to(equal([RACTuple tupleWithObjectsFromArray:@[]])); }); qck_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")); }); qck_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")); }); qck_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")); }); qck_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")); }); qck_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")); }); qck_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()); }); qck_it(@"should properly implement -respondsToSelector: when called on signalForSelector'd receiver that has subsequently been KVO'd", ^{ RACTestObject *object = [[RACTestObject alloc] init]; SEL selector = NSSelectorFromString(@"anyOldSelector:"); // Implement -anyOldSelector: on the object first [object rac_signalForSelector:selector]; expect(@([object respondsToSelector:selector])).to(beTruthy()); // Then KVO the object [[RACObserve(object, objectValue) publish] connect]; expect(@([object respondsToSelector:selector])).to(beTruthy()); }); qck_it(@"should properly implement -respondsToSelector: when called on signalForSelector'd receiver that has subsequently been KVO'd, then signalForSelector'd again", ^{ RACTestObject *object = [[RACTestObject alloc] init]; SEL selector = NSSelectorFromString(@"anyOldSelector:"); // Implement -anyOldSelector: on the object first [object rac_signalForSelector:selector]; expect(@([object respondsToSelector:selector])).to(beTruthy()); // Then KVO the object [[RACObserve(object, objectValue) publish] connect]; expect(@([object respondsToSelector:selector])).to(beTruthy()); SEL selector2 = NSSelectorFromString(@"anotherSelector:"); // Then implement -anotherSelector: on the object [object rac_signalForSelector:selector2]; expect(@([object respondsToSelector:selector2])).to(beTruthy()); }); qck_it(@"should call the right signal for two instances of the same class, adding signals for the same selector", ^{ RACTestObject *object1 = [[RACTestObject alloc] init]; RACTestObject *object2 = [[RACTestObject alloc] init]; SEL selector = NSSelectorFromString(@"lifeIsGood:"); __block id value1 = nil; [[object1 rac_signalForSelector:selector] subscribeNext:^(RACTuple *x) { value1 = x.first; }]; __block id value2 = nil; [[object2 rac_signalForSelector:selector] subscribeNext:^(RACTuple *x) { value2 = x.first; }]; [object1 lifeIsGood:@42]; expect(value1).to(equal(@42)); expect(value2).to(beNil()); [object2 lifeIsGood:@420]; expect(value1).to(equal(@42)); expect(value2).to(equal(@420)); }); qck_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()); }); qck_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)); }); qck_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()); }); }); qck_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:@[]])); }); qck_describe(@"a class that already overrides -forwardInvocation:", ^{ qck_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(@((size_t)(void*)object.forwardedSelector)).to(equal(@0)); [object performSelector:@selector(allObjects)]; expect(value).to(equal(@42)); expect(NSStringFromSelector(object.forwardedSelector)).to(equal(@"allObjects")); }); qck_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(@((size_t)(void*)object.forwardedSelector)).to(equal(@0)); [object performSelector:@selector(allObjects)]; expect(NSStringFromSelector(object.forwardedSelector)).to(equal(@"allObjects")); }); }); qck_describe(@"two classes in the same hierarchy", ^{ __block RACTestObject *superclassObj; __block RACTuple *superclassTuple; __block RACSubclassObject *subclassObj; __block RACTuple *subclassTuple; qck_beforeEach(^{ superclassObj = [[RACTestObject alloc] init]; expect(superclassObj).notTo(beNil()); subclassObj = [[RACSubclassObject alloc] init]; expect(subclassObj).notTo(beNil()); }); qck_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)); }); qck_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)).toEventually(beTruthy()); expectedValues = @[ @"foo", @"42" ]; expect(subclassTuple.allObjects).to(equal(expectedValues)); }); }); qck_describe(@"-rac_signalForSelector:fromProtocol", ^{ __block RACTestObject *object; __block Protocol *protocol; qck_beforeEach(^{ object = (id)[[RACTestObject alloc] init]; expect(object).notTo(beNil()); protocol = @protocol(TestProtocol); expect(protocol).notTo(beNil()); }); qck_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)); }); qck_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:@[]])); }); qck_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)); }); qck_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))); }); }); qck_describe(@"class reporting", ^{ __block RACTestObject *object; __block Class originalClass; qck_beforeEach(^{ object = [[RACTestObject alloc] init]; originalClass = object.class; }); qck_it(@"should report the original class", ^{ [object rac_signalForSelector:@selector(lifeIsGood:)]; expect(object.class).to(beIdenticalTo(originalClass)); }); qck_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)); }); qck_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)); }); }); QuickSpecEnd