blob: dc207efe5cd76880e183aafc5ec49f227f29ecec [file] [log] [blame]
//
// RACKVOWrapperSpec.m
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2012-08-07.
// Copyright (c) 2012 GitHub, Inc. All rights reserved.
//
#import "NSObject+RACKVOWrapper.h"
#import "EXTKeyPathCoding.h"
#import "NSObject+RACDeallocating.h"
#import "RACCompoundDisposable.h"
#import "RACDisposable.h"
#import "RACKVOTrampoline.h"
#import "RACTestObject.h"
@interface RACTestOperation : NSOperation
@end
// The name of the examples.
static NSString * const RACKVOWrapperExamples = @"RACKVOWrapperExamples";
// A block that returns an object to observe in the examples.
static NSString * const RACKVOWrapperExamplesTargetBlock = @"RACKVOWrapperExamplesTargetBlock";
// The key path to observe in the examples.
//
// The key path must have at least one weak property in it.
static NSString * const RACKVOWrapperExamplesKeyPath = @"RACKVOWrapperExamplesKeyPath";
// A block that changes the value of a weak property in the observed key path.
// The block is passed the object the example is observing and the new value the
// weak property should be changed to.
static NSString * const RACKVOWrapperExamplesChangeBlock = @"RACKVOWrapperExamplesChangeBlock";
// A block that returns a valid value for the weak property changed by
// RACKVOWrapperExamplesChangeBlock. The value must deallocate
// normally.
static NSString * const RACKVOWrapperExamplesValueBlock = @"RACKVOWrapperExamplesValueBlock";
// Whether RACKVOWrapperExamplesChangeBlock changes the value
// of the last key path component in the key path directly.
static NSString * const RACKVOWrapperExamplesChangesValueDirectly = @"RACKVOWrapperExamplesChangesValueDirectly";
// The name of the examples.
static NSString * const RACKVOWrapperCollectionExamples = @"RACKVOWrapperCollectionExamples";
// A block that returns an object to observe in the examples.
static NSString * const RACKVOWrapperCollectionExamplesTargetBlock = @"RACKVOWrapperCollectionExamplesTargetBlock";
// The key path to observe in the examples.
//
// Must identify a property of type NSOrderedSet.
static NSString * const RACKVOWrapperCollectionExamplesKeyPath = @"RACKVOWrapperCollectionExamplesKeyPath";
SharedExampleGroupsBegin(RACKVOWrapperExamples)
sharedExamplesFor(RACKVOWrapperExamples, ^(NSDictionary *data) {
__block NSObject *target = nil;
__block NSString *keyPath = nil;
__block void (^changeBlock)(NSObject *, id) = nil;
__block id (^valueBlock)(void) = nil;
__block BOOL changesValueDirectly = NO;
__block NSUInteger priorCallCount = 0;
__block NSUInteger posteriorCallCount = 0;
__block BOOL priorTriggeredByLastKeyPathComponent = NO;
__block BOOL posteriorTriggeredByLastKeyPathComponent = NO;
__block BOOL posteriorTriggeredByDeallocation = NO;
__block void (^callbackBlock)(id, NSDictionary *) = nil;
beforeEach(^{
NSObject * (^targetBlock)(void) = data[RACKVOWrapperExamplesTargetBlock];
target = targetBlock();
keyPath = data[RACKVOWrapperExamplesKeyPath];
changeBlock = data[RACKVOWrapperExamplesChangeBlock];
valueBlock = data[RACKVOWrapperExamplesValueBlock];
changesValueDirectly = [data[RACKVOWrapperExamplesChangesValueDirectly] boolValue];
priorCallCount = 0;
posteriorCallCount = 0;
callbackBlock = [^(id value, NSDictionary *change) {
if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
priorTriggeredByLastKeyPathComponent = [change[RACKeyValueChangeAffectedOnlyLastComponentKey] boolValue];
++priorCallCount;
return;
}
posteriorTriggeredByLastKeyPathComponent = [change[RACKeyValueChangeAffectedOnlyLastComponentKey] boolValue];
posteriorTriggeredByDeallocation = [change[RACKeyValueChangeCausedByDeallocationKey] boolValue];
++posteriorCallCount;
} copy];
});
afterEach(^{
target = nil;
keyPath = nil;
changeBlock = nil;
valueBlock = nil;
changesValueDirectly = NO;
callbackBlock = nil;
});
it(@"should not call the callback block on add if called without NSKeyValueObservingOptionInitial", ^{
[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior observer:nil block:callbackBlock];
expect(priorCallCount).to.equal(0);
expect(posteriorCallCount).to.equal(0);
});
it(@"should call the callback block on add if called with NSKeyValueObservingOptionInitial", ^{
[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior | NSKeyValueObservingOptionInitial observer:nil block:callbackBlock];
expect(priorCallCount).to.equal(0);
expect(posteriorCallCount).to.equal(1);
});
it(@"should call the callback block twice per change, once prior and once posterior", ^{
[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior observer:nil block:callbackBlock];
priorCallCount = 0;
posteriorCallCount = 0;
id value1 = valueBlock();
changeBlock(target, value1);
expect(priorCallCount).to.equal(1);
expect(posteriorCallCount).to.equal(1);
expect(priorTriggeredByLastKeyPathComponent).to.equal(changesValueDirectly);
expect(posteriorTriggeredByLastKeyPathComponent).to.equal(changesValueDirectly);
expect(posteriorTriggeredByDeallocation).to.beFalsy();
id value2 = valueBlock();
changeBlock(target, value2);
expect(priorCallCount).to.equal(2);
expect(posteriorCallCount).to.equal(2);
expect(priorTriggeredByLastKeyPathComponent).to.equal(changesValueDirectly);
expect(posteriorTriggeredByLastKeyPathComponent).to.equal(changesValueDirectly);
expect(posteriorTriggeredByDeallocation).to.beFalsy();
});
it(@"should call the callback block with NSKeyValueChangeNotificationIsPriorKey set before the value is changed, and not set after the value is changed", ^{
__block BOOL priorCalled = NO;
__block BOOL posteriorCalled = NO;
__block id priorValue = nil;
__block id posteriorValue = nil;
id value1 = valueBlock();
changeBlock(target, value1);
id oldValue = [target valueForKeyPath:keyPath];
[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior observer:nil block:^(id value, NSDictionary *change) {
if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
priorCalled = YES;
priorValue = value;
expect(posteriorCalled).to.beFalsy();
return;
}
posteriorCalled = YES;
posteriorValue = value;
expect(priorCalled).to.beTruthy();
}];
id value2 = valueBlock();
changeBlock(target, value2);
id newValue = [target valueForKeyPath:keyPath];
expect(priorCalled).to.beTruthy();
expect(priorValue).to.equal(oldValue);
expect(posteriorCalled).to.beTruthy();
expect(posteriorValue).to.equal(newValue);
});
it(@"should not call the callback block after it's been disposed", ^{
RACDisposable *disposable = [target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior observer:nil block:callbackBlock];
priorCallCount = 0;
posteriorCallCount = 0;
[disposable dispose];
expect(priorCallCount).to.equal(0);
expect(posteriorCallCount).to.equal(0);
id value = valueBlock();
changeBlock(target, value);
expect(priorCallCount).to.equal(0);
expect(posteriorCallCount).to.equal(0);
});
it(@"should call the callback block only once with NSKeyValueChangeNotificationIsPriorKey not set when the value is deallocated", ^{
__block BOOL valueDidDealloc = NO;
[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior observer:nil block:callbackBlock];
@autoreleasepool {
NSObject *value __attribute__((objc_precise_lifetime)) = valueBlock();
[value.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
valueDidDealloc = YES;
}]];
changeBlock(target, value);
priorCallCount = 0;
posteriorCallCount = 0;
}
expect(valueDidDealloc).to.beTruthy();
expect(priorCallCount).to.equal(0);
expect(posteriorCallCount).to.equal(1);
expect(posteriorTriggeredByDeallocation).to.beTruthy();
});
});
sharedExamplesFor(RACKVOWrapperCollectionExamples, ^(NSDictionary *data) {
__block NSObject *target = nil;
__block NSString *keyPath = nil;
__block NSMutableOrderedSet *mutableKeyPathProxy = nil;
__block void (^callbackBlock)(id, NSDictionary *) = nil;
__block id priorValue = nil;
__block id posteriorValue = nil;
__block NSDictionary *priorChange = nil;
__block NSDictionary *posteriorChange = nil;
beforeEach(^{
NSObject * (^targetBlock)(void) = data[RACKVOWrapperCollectionExamplesTargetBlock];
target = targetBlock();
keyPath = data[RACKVOWrapperCollectionExamplesKeyPath];
callbackBlock = [^(id value, NSDictionary *change) {
if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
priorValue = value;
priorChange = change;
return;
}
posteriorValue = value;
posteriorChange = change;
} copy];
[target setValue:[NSOrderedSet orderedSetWithObject:@0] forKeyPath:keyPath];
[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior observer:nil block:callbackBlock];
mutableKeyPathProxy = [target mutableOrderedSetValueForKeyPath:keyPath];
});
afterEach(^{
target = nil;
keyPath = nil;
callbackBlock = nil;
priorValue = nil;
priorChange = nil;
posteriorValue = nil;
posteriorChange = nil;
});
it(@"should support inserting elements into ordered collections", ^{
[mutableKeyPathProxy insertObject:@1 atIndex:0];
expect(priorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @0 ]]);
expect(posteriorValue).to.equal([NSOrderedSet orderedSetWithArray:(@[ @1, @0 ])]);
expect(priorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeInsertion);
expect(posteriorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeInsertion);
expect(priorChange[NSKeyValueChangeOldKey]).to.beNil();
expect(posteriorChange[NSKeyValueChangeNewKey]).to.equal(@[ @1 ]);
expect(priorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
expect(posteriorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
});
it(@"should support removing elements from ordered collections", ^{
[mutableKeyPathProxy removeObjectAtIndex:0];
expect(priorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @0 ]]);
expect(posteriorValue).to.equal([NSOrderedSet orderedSetWithArray:@[]]);
expect(priorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeRemoval);
expect(posteriorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeRemoval);
expect(priorChange[NSKeyValueChangeOldKey]).to.equal(@[ @0 ]);
expect(posteriorChange[NSKeyValueChangeNewKey]).to.beNil();
expect(priorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
expect(posteriorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
});
it(@"should support replacing elements in ordered collections", ^{
[mutableKeyPathProxy replaceObjectAtIndex:0 withObject:@1];
expect(priorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @0 ]]);
expect(posteriorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @1 ]]);
expect(priorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeReplacement);
expect(posteriorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeReplacement);
expect(priorChange[NSKeyValueChangeOldKey]).to.equal(@[ @0 ]);
expect(posteriorChange[NSKeyValueChangeNewKey]).to.equal(@[ @1 ]);
expect(priorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
expect(posteriorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
});
it(@"should support adding elements to unordered collections", ^{
[mutableKeyPathProxy unionOrderedSet:[NSOrderedSet orderedSetWithObject:@1]];
expect(priorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @0 ]]);
expect(posteriorValue).to.equal([NSOrderedSet orderedSetWithArray:(@[ @0, @1 ])]);
expect(priorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeInsertion);
expect(posteriorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeInsertion);
expect(priorChange[NSKeyValueChangeOldKey]).to.beNil();
expect(posteriorChange[NSKeyValueChangeNewKey]).to.equal(@[ @1 ]);
});
it(@"should support removing elements from unordered collections", ^{
[mutableKeyPathProxy minusOrderedSet:[NSOrderedSet orderedSetWithObject:@0]];
expect(priorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @0 ]]);
expect(posteriorValue).to.equal([NSOrderedSet orderedSetWithArray:@[]]);
expect(priorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeRemoval);
expect(posteriorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeRemoval);
expect(priorChange[NSKeyValueChangeOldKey]).to.equal(@[ @0 ]);
expect(posteriorChange[NSKeyValueChangeNewKey]).to.beNil();
});
});
SharedExampleGroupsEnd
SpecBegin(RACKVOWrapper)
describe(@"-rac_observeKeyPath:options:observer:block:", ^{
describe(@"on simple keys", ^{
NSObject * (^targetBlock)(void) = ^{
return [[RACTestObject alloc] init];
};
void (^changeBlock)(RACTestObject *, id) = ^(RACTestObject *target, id value) {
target.weakTestObjectValue = value;
};
id (^valueBlock)(void) = ^{
return [[RACTestObject alloc] init];
};
itShouldBehaveLike(RACKVOWrapperExamples, @{
RACKVOWrapperExamplesTargetBlock: targetBlock,
RACKVOWrapperExamplesKeyPath: @keypath(RACTestObject.new, weakTestObjectValue),
RACKVOWrapperExamplesChangeBlock: changeBlock,
RACKVOWrapperExamplesValueBlock: valueBlock,
RACKVOWrapperExamplesChangesValueDirectly: @YES
});
itShouldBehaveLike(RACKVOWrapperCollectionExamples, @{
RACKVOWrapperCollectionExamplesTargetBlock: targetBlock,
RACKVOWrapperCollectionExamplesKeyPath: @keypath(RACTestObject.new, orderedSetValue)
});
});
describe(@"on composite key paths'", ^{
describe(@"last key path components", ^{
NSObject *(^targetBlock)(void) = ^{
RACTestObject *object = [[RACTestObject alloc] init];
object.strongTestObjectValue = [[RACTestObject alloc] init];
return object;
};
void (^changeBlock)(RACTestObject *, id) = ^(RACTestObject *target, id value) {
target.strongTestObjectValue.weakTestObjectValue = value;
};
id (^valueBlock)(void) = ^{
return [[RACTestObject alloc] init];
};
itShouldBehaveLike(RACKVOWrapperExamples, @{
RACKVOWrapperExamplesTargetBlock: targetBlock,
RACKVOWrapperExamplesKeyPath: @keypath(RACTestObject.new, strongTestObjectValue.weakTestObjectValue),
RACKVOWrapperExamplesChangeBlock: changeBlock,
RACKVOWrapperExamplesValueBlock: valueBlock,
RACKVOWrapperExamplesChangesValueDirectly: @YES
});
itShouldBehaveLike(RACKVOWrapperCollectionExamples, @{
RACKVOWrapperCollectionExamplesTargetBlock: targetBlock,
RACKVOWrapperCollectionExamplesKeyPath: @keypath(RACTestObject.new, strongTestObjectValue.orderedSetValue)
});
});
describe(@"intermediate key path components", ^{
NSObject *(^targetBlock)(void) = ^{
return [[RACTestObject alloc] init];
};
void (^changeBlock)(RACTestObject *, id) = ^(RACTestObject *target, id value) {
target.weakTestObjectValue = value;
};
id (^valueBlock)(void) = ^{
RACTestObject *object = [[RACTestObject alloc] init];
object.strongTestObjectValue = [[RACTestObject alloc] init];
return object;
};
itShouldBehaveLike(RACKVOWrapperExamples, @{
RACKVOWrapperExamplesTargetBlock: targetBlock,
RACKVOWrapperExamplesKeyPath: @keypath([[RACTestObject alloc] init], weakTestObjectValue.strongTestObjectValue),
RACKVOWrapperExamplesChangeBlock: changeBlock,
RACKVOWrapperExamplesValueBlock: valueBlock,
RACKVOWrapperExamplesChangesValueDirectly: @NO
});
});
it(@"should not notice deallocation of the object returned by a dynamic final property", ^{
RACTestObject *object = [[RACTestObject alloc] init];
__block id lastValue = nil;
@autoreleasepool {
[object rac_observeKeyPath:@keypath(object.dynamicObjectProperty) options:NSKeyValueObservingOptionInitial observer:nil block:^(id value, NSDictionary *change) {
lastValue = value;
}];
expect(lastValue).to.beKindOf(RACTestObject.class);
}
expect(lastValue).to.beKindOf(RACTestObject.class);
});
it(@"should not notice deallocation of the object returned by a dynamic intermediate property", ^{
RACTestObject *object = [[RACTestObject alloc] init];
__block id lastValue = nil;
@autoreleasepool {
[object rac_observeKeyPath:@keypath(object.dynamicObjectProperty.integerValue) options:NSKeyValueObservingOptionInitial observer:nil block:^(id value, NSDictionary *change) {
lastValue = value;
}];
expect(lastValue).to.equal(@42);
}
expect(lastValue).to.equal(@42);
});
it(@"should not notice deallocation of the object returned by a dynamic method", ^{
RACTestObject *object = [[RACTestObject alloc] init];
__block id lastValue = nil;
@autoreleasepool {
[object rac_observeKeyPath:@keypath(object.dynamicObjectMethod) options:NSKeyValueObservingOptionInitial observer:nil block:^(id value, NSDictionary *change) {
lastValue = value;
}];
expect(lastValue).to.beKindOf(RACTestObject.class);
}
expect(lastValue).to.beKindOf(RACTestObject.class);
});
});
it(@"should not call the callback block when the value is the observer", ^{
__block BOOL observerDisposed = NO;
__block BOOL observerDeallocationTriggeredChange = NO;
__block BOOL targetDisposed = NO;
__block BOOL targetDeallocationTriggeredChange = NO;
@autoreleasepool {
RACTestObject *observer __attribute__((objc_precise_lifetime)) = [RACTestObject new];
[observer.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
observerDisposed = YES;
}]];
RACTestObject *target __attribute__((objc_precise_lifetime)) = [RACTestObject new];
[target.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
targetDisposed = YES;
}]];
observer.weakTestObjectValue = observer;
target.weakTestObjectValue = target;
// These observations can only result in dealloc triggered callbacks.
[observer rac_observeKeyPath:@keypath(target.weakTestObjectValue) options:0 observer:observer block:^(id _, NSDictionary *__) {
observerDeallocationTriggeredChange = YES;
}];
[target rac_observeKeyPath:@keypath(target.weakTestObjectValue) options:0 observer:observer block:^(id _, NSDictionary *__) {
targetDeallocationTriggeredChange = YES;
}];
}
expect(observerDisposed).to.beTruthy();
expect(observerDeallocationTriggeredChange).to.beFalsy();
expect(targetDisposed).to.beTruthy();
expect(targetDeallocationTriggeredChange).to.beTruthy();
});
it(@"should call the callback block for deallocation of the initial value of a single-key key path", ^{
RACTestObject *target = [RACTestObject new];
__block BOOL objectDisposed = NO;
__block BOOL objectDeallocationTriggeredChange = NO;
@autoreleasepool {
RACTestObject *object __attribute__((objc_precise_lifetime)) = [RACTestObject new];
target.weakTestObjectValue = object;
[object.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
objectDisposed = YES;
}]];
[target rac_observeKeyPath:@keypath(target.weakTestObjectValue) options:0 observer:target block:^(id _, NSDictionary *__) {
objectDeallocationTriggeredChange = YES;
}];
}
expect(objectDisposed).to.beTruthy();
expect(objectDeallocationTriggeredChange).to.beTruthy();
});
it(@"should call the callback block for deallocation of an object conforming to protocol property", ^{
RACTestObject *target = [RACTestObject new];
__block BOOL objectDisposed = NO;
__block BOOL objectDeallocationTriggeredChange = NO;
@autoreleasepool {
RACTestObject *object __attribute__((objc_precise_lifetime)) = [RACTestObject new];
target.weakObjectWithProtocol = object;
[object.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
objectDisposed = YES;
}]];
[target rac_observeKeyPath:@keypath(target.weakObjectWithProtocol) options:0 observer:target block:^(id _, NSDictionary *__) {
objectDeallocationTriggeredChange = YES;
}];
}
expect(objectDisposed).to.beTruthy();
expect(objectDeallocationTriggeredChange).to.beTruthy();
});
});
describe(@"rac_addObserver:forKeyPath:options:block:", ^{
it(@"should add and remove an observer", ^{
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{}];
expect(operation).notTo.beNil();
__block BOOL notified = NO;
RACDisposable *disposable = [operation rac_observeKeyPath:@"isFinished" options:NSKeyValueObservingOptionNew observer:self block:^(id value, NSDictionary *change) {
expect([change objectForKey:NSKeyValueChangeNewKey]).to.equal(@YES);
expect(notified).to.beFalsy();
notified = YES;
}];
expect(disposable).notTo.beNil();
[operation start];
[operation waitUntilFinished];
expect(notified).will.beTruthy();
});
it(@"should accept a nil observer", ^{
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{}];
RACDisposable *disposable = [operation rac_observeKeyPath:@"isFinished" options:NSKeyValueObservingOptionNew observer:nil block:^(id value, NSDictionary *change) {}];
expect(disposable).notTo.beNil();
});
it(@"automatically stops KVO on subclasses when the target deallocates", ^{
void (^testKVOOnSubclass)(Class targetClass, id observer) = ^(Class targetClass, id observer) {
__weak id weakTarget = nil;
__weak id identifier = nil;
@autoreleasepool {
// Create an observable target that we control the memory management of.
CFTypeRef target = CFBridgingRetain([[targetClass alloc] init]);
expect(target).notTo.beNil();
weakTarget = (__bridge id)target;
expect(weakTarget).notTo.beNil();
identifier = [(__bridge id)target rac_observeKeyPath:@"isFinished" options:0 observer:observer block:^(id value, NSDictionary *change) {}];
expect(identifier).notTo.beNil();
CFRelease(target);
}
expect(weakTarget).to.beNil();
expect(identifier).to.beNil();
};
it (@"stops KVO on NSObject subclasses", ^{
testKVOOnSubclass(NSOperation.class, self);
});
it(@"stops KVO on subclasses of already-swizzled classes", ^{
testKVOOnSubclass(RACTestOperation.class, self);
});
it (@"stops KVO on NSObject subclasses even with a nil observer", ^{
testKVOOnSubclass(NSOperation.class, nil);
});
it(@"stops KVO on subclasses of already-swizzled classes even with a nil observer", ^{
testKVOOnSubclass(RACTestOperation.class, nil);
});
});
it(@"should automatically stop KVO when the observer deallocates", ^{
__weak id weakObserver = nil;
__weak id identifier = nil;
NSOperation *operation = [[NSOperation alloc] init];
@autoreleasepool {
// Create an observer that we control the memory management of.
CFTypeRef observer = CFBridgingRetain([[NSOperation alloc] init]);
expect(observer).notTo.beNil();
weakObserver = (__bridge id)observer;
expect(weakObserver).notTo.beNil();
identifier = [operation rac_observeKeyPath:@"isFinished" options:0 observer:(__bridge id)observer block:^(id value, NSDictionary *change) {}];
expect(identifier).notTo.beNil();
CFRelease(observer);
}
expect(weakObserver).to.beNil();
});
it(@"should stop KVO when the observer is disposed", ^{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block NSString *name = nil;
RACDisposable *disposable = [queue rac_observeKeyPath:@"name" options:0 observer:self block:^(id value, NSDictionary *change) {
name = queue.name;
}];
queue.name = @"1";
expect(name).to.equal(@"1");
[disposable dispose];
queue.name = @"2";
expect(name).to.equal(@"1");
});
it(@"should distinguish between observers being disposed", ^{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block NSString *name1 = nil;
__block NSString *name2 = nil;
RACDisposable *disposable = [queue rac_observeKeyPath:@"name" options:0 observer:self block:^(id value, NSDictionary *change) {
name1 = queue.name;
}];
[queue rac_observeKeyPath:@"name" options:0 observer:self block:^(id value, NSDictionary *change) {
name2 = queue.name;
}];
queue.name = @"1";
expect(name1).to.equal(@"1");
expect(name2).to.equal(@"1");
[disposable dispose];
queue.name = @"2";
expect(name1).to.equal(@"1");
expect(name2).to.equal(@"2");
});
});
SpecEnd
@implementation RACTestOperation
@end