| // | |
| // 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 |