blob: b5b18398ba80715ec222bc173116d04b19d18b60 [file] [log] [blame] [edit]
//
// 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