| // |
| // RACKVOChannelSpec.m |
| // ReactiveCocoa |
| // |
| // Created by Uri Baghin on 16/12/2012. |
| // Copyright (c) 2012 GitHub, Inc. All rights reserved. |
| // |
| |
| #import "RACTestObject.h" |
| #import "RACChannelExamples.h" |
| #import "RACPropertySignalExamples.h" |
| |
| #import "NSObject+RACDeallocating.h" |
| #import "NSObject+RACKVOWrapper.h" |
| #import "RACCompoundDisposable.h" |
| #import "RACDisposable.h" |
| #import "RACKVOChannel.h" |
| #import "RACSignal+Operations.h" |
| |
| SpecBegin(RACKVOChannel) |
| |
| describe(@"RACKVOChannel", ^{ |
| __block RACTestObject *object; |
| __block RACKVOChannel *channel; |
| id value1 = @"test value 1"; |
| id value2 = @"test value 2"; |
| id value3 = @"test value 3"; |
| NSArray *values = @[ value1, value2, value3 ]; |
| |
| before(^{ |
| object = [[RACTestObject alloc] init]; |
| channel = [[RACKVOChannel alloc] initWithTarget:object keyPath:@keypath(object.stringValue) nilValue:nil]; |
| }); |
| |
| id setupBlock = ^(RACTestObject *testObject, NSString *keyPath, id nilValue, RACSignal *signal) { |
| RACKVOChannel *channel = [[RACKVOChannel alloc] initWithTarget:testObject keyPath:keyPath nilValue:nilValue]; |
| [signal subscribe:channel.followingTerminal]; |
| }; |
| |
| itShouldBehaveLike(RACPropertySignalExamples, ^{ |
| return @{ RACPropertySignalExamplesSetupBlock: setupBlock }; |
| }); |
| |
| itShouldBehaveLike(RACChannelExamples, @{ |
| RACChannelExampleCreateBlock: [^{ |
| return [[RACKVOChannel alloc] initWithTarget:object keyPath:@keypath(object.stringValue) nilValue:nil]; |
| } copy] |
| }); |
| |
| it(@"should send the object's current value when subscribed to followingTerminal", ^{ |
| __block id receivedValue = @"received value should not be this"; |
| [[channel.followingTerminal take:1] subscribeNext:^(id x) { |
| receivedValue = x; |
| }]; |
| |
| expect(receivedValue).to.beNil(); |
| |
| object.stringValue = value1; |
| [[channel.followingTerminal take:1] subscribeNext:^(id x) { |
| receivedValue = x; |
| }]; |
| |
| expect(receivedValue).to.equal(value1); |
| }); |
| |
| it(@"should send the object's new value on followingTerminal when it's changed", ^{ |
| object.stringValue = value1; |
| |
| NSMutableArray *receivedValues = [NSMutableArray array]; |
| [channel.followingTerminal subscribeNext:^(id x) { |
| [receivedValues addObject:x]; |
| }]; |
| |
| object.stringValue = value2; |
| object.stringValue = value3; |
| expect(receivedValues).to.equal(values); |
| }); |
| |
| it(@"should set the object's value using values sent to the followingTerminal", ^{ |
| expect(object.stringValue).to.beNil(); |
| |
| [channel.followingTerminal sendNext:value1]; |
| expect(object.stringValue).to.equal(value1); |
| |
| [channel.followingTerminal sendNext:value2]; |
| expect(object.stringValue).to.equal(value2); |
| }); |
| |
| it(@"should be able to subscribe to signals", ^{ |
| NSMutableArray *receivedValues = [NSMutableArray array]; |
| [object rac_observeKeyPath:@keypath(object.stringValue) options:0 observer:self block:^(id value, NSDictionary *change) { |
| [receivedValues addObject:value]; |
| }]; |
| |
| RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) { |
| [subscriber sendNext:value1]; |
| [subscriber sendNext:value2]; |
| [subscriber sendNext:value3]; |
| return nil; |
| }]; |
| |
| [signal subscribe:channel.followingTerminal]; |
| expect(receivedValues).to.equal(values); |
| }); |
| |
| it(@"should complete both terminals when the target deallocates", ^{ |
| __block BOOL leadingCompleted = NO; |
| __block BOOL followingCompleted = NO; |
| __block BOOL deallocated = NO; |
| |
| @autoreleasepool { |
| RACTestObject *object __attribute__((objc_precise_lifetime)) = [[RACTestObject alloc] init]; |
| [object.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ |
| deallocated = YES; |
| }]]; |
| |
| RACKVOChannel *channel = [[RACKVOChannel alloc] initWithTarget:object keyPath:@keypath(object.stringValue) nilValue:nil]; |
| [channel.leadingTerminal subscribeCompleted:^{ |
| leadingCompleted = YES; |
| }]; |
| |
| [channel.followingTerminal subscribeCompleted:^{ |
| followingCompleted = YES; |
| }]; |
| |
| expect(deallocated).to.beFalsy(); |
| expect(leadingCompleted).to.beFalsy(); |
| expect(followingCompleted).to.beFalsy(); |
| } |
| |
| expect(deallocated).to.beTruthy(); |
| expect(leadingCompleted).to.beTruthy(); |
| expect(followingCompleted).to.beTruthy(); |
| }); |
| |
| it(@"should deallocate when the target deallocates", ^{ |
| __block BOOL targetDeallocated = NO; |
| __block BOOL channelDeallocated = NO; |
| |
| @autoreleasepool { |
| RACTestObject *object __attribute__((objc_precise_lifetime)) = [[RACTestObject alloc] init]; |
| [object.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ |
| targetDeallocated = YES; |
| }]]; |
| |
| RACKVOChannel *channel = [[RACKVOChannel alloc] initWithTarget:object keyPath:@keypath(object.stringValue) nilValue:nil]; |
| [channel.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ |
| channelDeallocated = YES; |
| }]]; |
| |
| expect(targetDeallocated).to.beFalsy(); |
| expect(channelDeallocated).to.beFalsy(); |
| } |
| |
| expect(targetDeallocated).to.beTruthy(); |
| expect(channelDeallocated).to.beTruthy(); |
| }); |
| }); |
| |
| describe(@"RACChannelTo", ^{ |
| __block RACTestObject *a; |
| __block RACTestObject *b; |
| __block RACTestObject *c; |
| __block NSString *testName1; |
| __block NSString *testName2; |
| __block NSString *testName3; |
| |
| before(^{ |
| a = [[RACTestObject alloc] init]; |
| b = [[RACTestObject alloc] init]; |
| c = [[RACTestObject alloc] init]; |
| testName1 = @"sync it!"; |
| testName2 = @"sync it again!"; |
| testName3 = @"sync it once more!"; |
| }); |
| |
| it(@"should keep objects' properties in sync", ^{ |
| RACChannelTo(a, stringValue) = RACChannelTo(b, stringValue); |
| expect(a.stringValue).to.beNil(); |
| expect(b.stringValue).to.beNil(); |
| |
| a.stringValue = testName1; |
| expect(a.stringValue).to.equal(testName1); |
| expect(b.stringValue).to.equal(testName1); |
| |
| b.stringValue = testName2; |
| expect(a.stringValue).to.equal(testName2); |
| expect(b.stringValue).to.equal(testName2); |
| |
| a.stringValue = nil; |
| expect(a.stringValue).to.beNil(); |
| expect(b.stringValue).to.beNil(); |
| }); |
| |
| it(@"should keep properties identified by keypaths in sync", ^{ |
| RACChannelTo(a, strongTestObjectValue.stringValue) = RACChannelTo(b, strongTestObjectValue.stringValue); |
| a.strongTestObjectValue = [[RACTestObject alloc] init]; |
| b.strongTestObjectValue = [[RACTestObject alloc] init]; |
| |
| a.strongTestObjectValue.stringValue = testName1; |
| expect(a.strongTestObjectValue.stringValue).to.equal(testName1); |
| expect(b.strongTestObjectValue.stringValue).to.equal(testName1); |
| expect(a.strongTestObjectValue).notTo.equal(b.strongTestObjectValue); |
| |
| b.strongTestObjectValue = nil; |
| expect(a.strongTestObjectValue.stringValue).to.beNil(); |
| |
| c.stringValue = testName2; |
| b.strongTestObjectValue = c; |
| expect(a.strongTestObjectValue.stringValue).to.equal(testName2); |
| expect(b.strongTestObjectValue.stringValue).to.equal(testName2); |
| expect(a.strongTestObjectValue).notTo.equal(b.strongTestObjectValue); |
| }); |
| |
| it(@"should update properties identified by keypaths when the intermediate values change", ^{ |
| RACChannelTo(a, strongTestObjectValue.stringValue) = RACChannelTo(b, strongTestObjectValue.stringValue); |
| a.strongTestObjectValue = [[RACTestObject alloc] init]; |
| b.strongTestObjectValue = [[RACTestObject alloc] init]; |
| c.stringValue = testName1; |
| b.strongTestObjectValue = c; |
| |
| expect(a.strongTestObjectValue.stringValue).to.equal(testName1); |
| expect(a.strongTestObjectValue).notTo.equal(b.strongTestObjectValue); |
| }); |
| |
| it(@"should update properties identified by keypaths when the channel was created when one of the two objects had an intermediate nil value", ^{ |
| RACChannelTo(a, strongTestObjectValue.stringValue) = RACChannelTo(b, strongTestObjectValue.stringValue); |
| b.strongTestObjectValue = [[RACTestObject alloc] init]; |
| c.stringValue = testName1; |
| a.strongTestObjectValue = c; |
| |
| expect(a.strongTestObjectValue.stringValue).to.equal(testName1); |
| expect(b.strongTestObjectValue.stringValue).to.equal(testName1); |
| expect(a.strongTestObjectValue).notTo.equal(b.strongTestObjectValue); |
| }); |
| |
| it(@"should take the value of the object being bound to at the start", ^{ |
| a.stringValue = testName1; |
| b.stringValue = testName2; |
| |
| RACChannelTo(a, stringValue) = RACChannelTo(b, stringValue); |
| expect(a.stringValue).to.equal(testName2); |
| expect(b.stringValue).to.equal(testName2); |
| }); |
| |
| it(@"should update the value even if it's the same value the object had before it was bound", ^{ |
| a.stringValue = testName1; |
| b.stringValue = testName2; |
| |
| RACChannelTo(a, stringValue) = RACChannelTo(b, stringValue); |
| expect(a.stringValue).to.equal(testName2); |
| expect(b.stringValue).to.equal(testName2); |
| |
| b.stringValue = testName1; |
| expect(a.stringValue).to.equal(testName1); |
| expect(b.stringValue).to.equal(testName1); |
| }); |
| |
| it(@"should bind transitively", ^{ |
| a.stringValue = testName1; |
| b.stringValue = testName2; |
| c.stringValue = testName3; |
| |
| RACChannelTo(a, stringValue) = RACChannelTo(b, stringValue); |
| RACChannelTo(b, stringValue) = RACChannelTo(c, stringValue); |
| expect(a.stringValue).to.equal(testName3); |
| expect(b.stringValue).to.equal(testName3); |
| expect(c.stringValue).to.equal(testName3); |
| |
| c.stringValue = testName1; |
| expect(a.stringValue).to.equal(testName1); |
| expect(b.stringValue).to.equal(testName1); |
| expect(c.stringValue).to.equal(testName1); |
| |
| b.stringValue = testName2; |
| expect(a.stringValue).to.equal(testName2); |
| expect(b.stringValue).to.equal(testName2); |
| expect(c.stringValue).to.equal(testName2); |
| |
| a.stringValue = testName3; |
| expect(a.stringValue).to.equal(testName3); |
| expect(b.stringValue).to.equal(testName3); |
| expect(c.stringValue).to.equal(testName3); |
| }); |
| |
| it(@"should bind changes made by KVC on arrays", ^{ |
| b.arrayValue = @[]; |
| RACChannelTo(a, arrayValue) = RACChannelTo(b, arrayValue); |
| |
| [[b mutableArrayValueForKeyPath:@keypath(b.arrayValue)] addObject:@1]; |
| expect(a.arrayValue).to.equal(b.arrayValue); |
| }); |
| |
| it(@"should bind changes made by KVC on sets", ^{ |
| b.setValue = [NSSet set]; |
| RACChannelTo(a, setValue) = RACChannelTo(b, setValue); |
| |
| [[b mutableSetValueForKeyPath:@keypath(b.setValue)] addObject:@1]; |
| expect(a.setValue).to.equal(b.setValue); |
| }); |
| |
| it(@"should bind changes made by KVC on ordered sets", ^{ |
| b.orderedSetValue = [NSOrderedSet orderedSet]; |
| RACChannelTo(a, orderedSetValue) = RACChannelTo(b, orderedSetValue); |
| |
| [[b mutableOrderedSetValueForKeyPath:@keypath(b.orderedSetValue)] addObject:@1]; |
| expect(a.orderedSetValue).to.equal(b.orderedSetValue); |
| }); |
| |
| it(@"should handle deallocation of intermediate objects correctly even without support from KVO", ^{ |
| __block BOOL wasDisposed = NO; |
| |
| RACChannelTo(a, weakTestObjectValue.stringValue) = RACChannelTo(b, strongTestObjectValue.stringValue); |
| b.strongTestObjectValue = [[RACTestObject alloc] init]; |
| |
| @autoreleasepool { |
| RACTestObject *object = [[RACTestObject alloc] init]; |
| [object.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ |
| wasDisposed = YES; |
| }]]; |
| |
| a.weakTestObjectValue = object; |
| object.stringValue = testName1; |
| |
| expect(wasDisposed).to.beFalsy(); |
| expect(b.strongTestObjectValue.stringValue).to.equal(testName1); |
| } |
| |
| expect(wasDisposed).will.beTruthy(); |
| expect(b.strongTestObjectValue.stringValue).to.beNil(); |
| }); |
| |
| it(@"should stop binding when disposed", ^{ |
| RACChannelTerminal *aTerminal = RACChannelTo(a, stringValue); |
| RACChannelTerminal *bTerminal = RACChannelTo(b, stringValue); |
| |
| a.stringValue = testName1; |
| RACDisposable *disposable = [aTerminal subscribe:bTerminal]; |
| |
| expect(a.stringValue).to.equal(testName1); |
| expect(b.stringValue).to.equal(testName1); |
| |
| a.stringValue = testName2; |
| expect(a.stringValue).to.equal(testName2); |
| expect(b.stringValue).to.equal(testName2); |
| |
| [disposable dispose]; |
| |
| a.stringValue = testName3; |
| expect(a.stringValue).to.equal(testName3); |
| expect(b.stringValue).to.equal(testName2); |
| }); |
| |
| it(@"should use the nilValue when sent nil", ^{ |
| RACChannelTerminal *terminal = RACChannelTo(a, integerValue, @5); |
| expect(a.integerValue).to.equal(0); |
| |
| [terminal sendNext:@2]; |
| expect(a.integerValue).to.equal(2); |
| |
| [terminal sendNext:nil]; |
| expect(a.integerValue).to.equal(5); |
| }); |
| |
| it(@"should use the nilValue when an intermediate object is nil", ^{ |
| __block BOOL wasDisposed = NO; |
| |
| RACChannelTo(a, weakTestObjectValue.integerValue, @5) = RACChannelTo(b, strongTestObjectValue.integerValue, @5); |
| b.strongTestObjectValue = [[RACTestObject alloc] init]; |
| |
| @autoreleasepool { |
| RACTestObject *object = [[RACTestObject alloc] init]; |
| [object.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ |
| wasDisposed = YES; |
| }]]; |
| |
| a.weakTestObjectValue = object; |
| object.integerValue = 2; |
| |
| expect(wasDisposed).to.beFalsy(); |
| expect(b.strongTestObjectValue.integerValue).to.equal(2); |
| } |
| |
| expect(wasDisposed).will.beTruthy(); |
| expect(b.strongTestObjectValue.integerValue).to.equal(5); |
| }); |
| }); |
| |
| SpecEnd |