| // |
| // RACCommandSpec.m |
| // ReactiveCocoa |
| // |
| // Created by Josh Abernathy on 8/31/12. |
| // Copyright (c) 2012 GitHub, Inc. All rights reserved. |
| // |
| |
| #import "NSArray+RACSequenceAdditions.h" |
| #import "NSObject+RACDeallocating.h" |
| #import "NSObject+RACPropertySubscribing.h" |
| #import "RACCommand.h" |
| #import "RACCompoundDisposable.h" |
| #import "RACDisposable.h" |
| #import "RACEvent.h" |
| #import "RACScheduler.h" |
| #import "RACSequence.h" |
| #import "RACSignal+Operations.h" |
| #import "RACSubject.h" |
| #import "RACUnit.h" |
| |
| SpecBegin(RACCommand) |
| |
| RACSignal * (^emptySignalBlock)(id) = ^(id _) { |
| return [RACSignal empty]; |
| }; |
| |
| describe(@"with a simple signal block", ^{ |
| __block RACCommand *command; |
| |
| beforeEach(^{ |
| command = [[RACCommand alloc] initWithSignalBlock:^(id value) { |
| return [RACSignal return:value]; |
| }]; |
| |
| expect(command).notTo.beNil(); |
| expect(command.allowsConcurrentExecution).to.beFalsy(); |
| }); |
| |
| it(@"should be enabled by default", ^{ |
| expect([command.enabled first]).to.equal(@YES); |
| }); |
| |
| it(@"should not be executing by default", ^{ |
| expect([command.executing first]).to.equal(@NO); |
| }); |
| |
| it(@"should create an execution signal", ^{ |
| __block NSUInteger signalsReceived = 0; |
| __block BOOL completed = NO; |
| |
| id value = NSNull.null; |
| [command.executionSignals subscribeNext:^(RACSignal *signal) { |
| signalsReceived++; |
| |
| [signal subscribeNext:^(id x) { |
| expect(x).to.equal(value); |
| } completed:^{ |
| completed = YES; |
| }]; |
| }]; |
| |
| expect(signalsReceived).to.equal(0); |
| |
| [command execute:value]; |
| expect(signalsReceived).will.equal(1); |
| expect(completed).to.beTruthy(); |
| }); |
| |
| it(@"should return the execution signal from -execute:", ^{ |
| __block BOOL completed = NO; |
| |
| id value = NSNull.null; |
| [[command |
| execute:value] |
| subscribeNext:^(id x) { |
| expect(x).to.equal(value); |
| } completed:^{ |
| completed = YES; |
| }]; |
| |
| expect(completed).will.beTruthy(); |
| }); |
| |
| it(@"should always send executionSignals on the main thread", ^{ |
| __block RACScheduler *receivedScheduler = nil; |
| [command.executionSignals subscribeNext:^(id _) { |
| receivedScheduler = RACScheduler.currentScheduler; |
| }]; |
| |
| [[RACScheduler scheduler] schedule:^{ |
| expect([[command execute:nil] waitUntilCompleted:NULL]).to.beTruthy(); |
| }]; |
| |
| expect(receivedScheduler).to.beNil(); |
| expect(receivedScheduler).will.equal(RACScheduler.mainThreadScheduler); |
| }); |
| |
| it(@"should not send anything on 'errors' by default", ^{ |
| __block BOOL receivedError = NO; |
| [command.errors subscribeNext:^(id _) { |
| receivedError = YES; |
| }]; |
| |
| expect([[command execute:nil] asynchronouslyWaitUntilCompleted:NULL]).to.beTruthy(); |
| expect(receivedError).to.beFalsy(); |
| }); |
| |
| it(@"should be executing while an execution signal is running", ^{ |
| [command.executionSignals subscribeNext:^(RACSignal *signal) { |
| [signal subscribeNext:^(id x) { |
| expect([command.executing first]).to.equal(@YES); |
| }]; |
| }]; |
| |
| expect([[command execute:nil] asynchronouslyWaitUntilCompleted:NULL]).to.beTruthy(); |
| expect([command.executing first]).to.equal(@NO); |
| }); |
| |
| it(@"should always update executing on the main thread", ^{ |
| __block RACScheduler *updatedScheduler = nil; |
| [[command.executing skip:1] subscribeNext:^(NSNumber *executing) { |
| if (!executing.boolValue) return; |
| |
| updatedScheduler = RACScheduler.currentScheduler; |
| }]; |
| |
| [[RACScheduler scheduler] schedule:^{ |
| expect([[command execute:nil] waitUntilCompleted:NULL]).to.beTruthy(); |
| }]; |
| |
| expect([command.executing first]).to.equal(@NO); |
| expect(updatedScheduler).will.equal(RACScheduler.mainThreadScheduler); |
| }); |
| |
| it(@"should dealloc without subscribers", ^{ |
| __block BOOL disposed = NO; |
| |
| @autoreleasepool { |
| RACCommand *command __attribute__((objc_precise_lifetime)) = [[RACCommand alloc] initWithSignalBlock:emptySignalBlock]; |
| [command.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ |
| disposed = YES; |
| }]]; |
| } |
| |
| expect(disposed).will.beTruthy(); |
| }); |
| |
| it(@"should complete signals on the main thread when deallocated", ^{ |
| __block RACScheduler *executionSignalsScheduler = nil; |
| __block RACScheduler *executingScheduler = nil; |
| __block RACScheduler *enabledScheduler = nil; |
| __block RACScheduler *errorsScheduler = nil; |
| |
| [[RACScheduler scheduler] schedule:^{ |
| @autoreleasepool { |
| RACCommand *command __attribute__((objc_precise_lifetime)) = [[RACCommand alloc] initWithSignalBlock:emptySignalBlock]; |
| |
| [command.executionSignals subscribeCompleted:^{ |
| executionSignalsScheduler = RACScheduler.currentScheduler; |
| }]; |
| |
| [command.executing subscribeCompleted:^{ |
| executingScheduler = RACScheduler.currentScheduler; |
| }]; |
| |
| [command.enabled subscribeCompleted:^{ |
| enabledScheduler = RACScheduler.currentScheduler; |
| }]; |
| |
| [command.errors subscribeCompleted:^{ |
| errorsScheduler = RACScheduler.currentScheduler; |
| }]; |
| } |
| }]; |
| |
| expect(executionSignalsScheduler).will.equal(RACScheduler.mainThreadScheduler); |
| expect(executingScheduler).will.equal(RACScheduler.mainThreadScheduler); |
| expect(enabledScheduler).will.equal(RACScheduler.mainThreadScheduler); |
| expect(errorsScheduler).will.equal(RACScheduler.mainThreadScheduler); |
| }); |
| }); |
| |
| it(@"should invoke the signalBlock once per execution", ^{ |
| NSMutableArray *valuesReceived = [NSMutableArray array]; |
| RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(id x) { |
| [valuesReceived addObject:x]; |
| return [RACSignal empty]; |
| }]; |
| |
| expect([[command execute:@"foo"] asynchronouslyWaitUntilCompleted:NULL]).to.beTruthy(); |
| expect(valuesReceived).to.equal((@[ @"foo" ])); |
| |
| expect([[command execute:@"bar"] asynchronouslyWaitUntilCompleted:NULL]).to.beTruthy(); |
| expect(valuesReceived).to.equal((@[ @"foo", @"bar" ])); |
| }); |
| |
| it(@"should send on executionSignals in order of execution", ^{ |
| RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(RACSequence *seq) { |
| return [seq signalWithScheduler:RACScheduler.immediateScheduler]; |
| }]; |
| |
| NSMutableArray *valuesReceived = [NSMutableArray array]; |
| [[command.executionSignals |
| concat] |
| subscribeNext:^(id x) { |
| [valuesReceived addObject:x]; |
| }]; |
| |
| RACSequence *first = @[ @"foo", @"bar" ].rac_sequence; |
| expect([[command execute:first] asynchronouslyWaitUntilCompleted:NULL]).to.beTruthy(); |
| |
| RACSequence *second = @[ @"buzz", @"baz" ].rac_sequence; |
| expect([[command execute:second] asynchronouslyWaitUntilCompleted:NULL]).will.beTruthy(); |
| |
| NSArray *expectedValues = @[ @"foo", @"bar", @"buzz", @"baz" ]; |
| expect(valuesReceived).to.equal(expectedValues); |
| }); |
| |
| it(@"should wait for all signals to complete or error before executing sends NO", ^{ |
| RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(RACSignal *signal) { |
| return signal; |
| }]; |
| |
| command.allowsConcurrentExecution = YES; |
| |
| RACSubject *firstSubject = [RACSubject subject]; |
| expect([command execute:firstSubject]).notTo.beNil(); |
| |
| RACSubject *secondSubject = [RACSubject subject]; |
| expect([command execute:secondSubject]).notTo.beNil(); |
| |
| expect([command.executing first]).will.equal(@YES); |
| |
| [firstSubject sendError:nil]; |
| expect([command.executing first]).to.equal(@YES); |
| |
| [secondSubject sendNext:nil]; |
| expect([command.executing first]).to.equal(@YES); |
| |
| [secondSubject sendCompleted]; |
| expect([command.executing first]).will.equal(@NO); |
| }); |
| |
| it(@"should not deliver errors from executionSignals", ^{ |
| RACSubject *subject = [RACSubject subject]; |
| NSMutableArray *receivedEvents = [NSMutableArray array]; |
| |
| RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(id _) { |
| return subject; |
| }]; |
| |
| [[[command.executionSignals |
| flatten] |
| materialize] |
| subscribeNext:^(RACEvent *event) { |
| [receivedEvents addObject:event]; |
| }]; |
| |
| expect([command execute:nil]).notTo.beNil(); |
| expect([command.executing first]).will.equal(@YES); |
| |
| [subject sendNext:RACUnit.defaultUnit]; |
| |
| NSArray *expectedEvents = @[ [RACEvent eventWithValue:RACUnit.defaultUnit] ]; |
| expect(receivedEvents).will.equal(expectedEvents); |
| expect([command.executing first]).to.equal(@YES); |
| |
| [subject sendNext:@"foo"]; |
| |
| expectedEvents = @[ [RACEvent eventWithValue:RACUnit.defaultUnit], [RACEvent eventWithValue:@"foo"] ]; |
| expect(receivedEvents).will.equal(expectedEvents); |
| expect([command.executing first]).to.equal(@YES); |
| |
| NSError *error = [NSError errorWithDomain:@"" code:1 userInfo:nil]; |
| [subject sendError:error]; |
| |
| expect([command.executing first]).will.equal(@NO); |
| expect(receivedEvents).to.equal(expectedEvents); |
| }); |
| |
| it(@"should deliver errors from -execute:", ^{ |
| RACSubject *subject = [RACSubject subject]; |
| NSMutableArray *receivedEvents = [NSMutableArray array]; |
| |
| RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(id _) { |
| return subject; |
| }]; |
| |
| [[[command |
| execute:nil] |
| materialize] |
| subscribeNext:^(RACEvent *event) { |
| [receivedEvents addObject:event]; |
| }]; |
| |
| expect([command.executing first]).will.equal(@YES); |
| |
| [subject sendNext:RACUnit.defaultUnit]; |
| |
| NSArray *expectedEvents = @[ [RACEvent eventWithValue:RACUnit.defaultUnit] ]; |
| expect(receivedEvents).will.equal(expectedEvents); |
| expect([command.executing first]).to.equal(@YES); |
| |
| [subject sendNext:@"foo"]; |
| |
| expectedEvents = @[ [RACEvent eventWithValue:RACUnit.defaultUnit], [RACEvent eventWithValue:@"foo"] ]; |
| expect(receivedEvents).will.equal(expectedEvents); |
| expect([command.executing first]).to.equal(@YES); |
| |
| NSError *error = [NSError errorWithDomain:@"" code:1 userInfo:nil]; |
| [subject sendError:error]; |
| |
| expectedEvents = @[ [RACEvent eventWithValue:RACUnit.defaultUnit], [RACEvent eventWithValue:@"foo"], [RACEvent eventWithError:error] ]; |
| expect(receivedEvents).will.equal(expectedEvents); |
| expect([command.executing first]).will.equal(@NO); |
| }); |
| |
| it(@"should deliver errors onto 'errors'", ^{ |
| RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(RACSignal *signal) { |
| return signal; |
| }]; |
| |
| command.allowsConcurrentExecution = YES; |
| |
| RACSubject *firstSubject = [RACSubject subject]; |
| expect([command execute:firstSubject]).notTo.beNil(); |
| |
| RACSubject *secondSubject = [RACSubject subject]; |
| expect([command execute:secondSubject]).notTo.beNil(); |
| |
| NSError *firstError = [NSError errorWithDomain:@"" code:1 userInfo:nil]; |
| NSError *secondError = [NSError errorWithDomain:@"" code:2 userInfo:nil]; |
| |
| // We should receive errors from our previously-started executions. |
| NSMutableArray *receivedErrors = [NSMutableArray array]; |
| [command.errors subscribeNext:^(NSError *error) { |
| [receivedErrors addObject:error]; |
| }]; |
| |
| expect([command.executing first]).will.equal(@YES); |
| |
| [firstSubject sendError:firstError]; |
| expect([command.executing first]).will.equal(@YES); |
| |
| NSArray *expected = @[ firstError ]; |
| expect(receivedErrors).will.equal(expected); |
| |
| [secondSubject sendError:secondError]; |
| expect([command.executing first]).will.equal(@NO); |
| |
| expected = @[ firstError, secondError ]; |
| expect(receivedErrors).will.equal(expected); |
| }); |
| |
| it(@"should not deliver non-error events onto 'errors'", ^{ |
| RACSubject *subject = [RACSubject subject]; |
| RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(id _) { |
| return subject; |
| }]; |
| |
| __block BOOL receivedEvent = NO; |
| [command.errors subscribeNext:^(id _) { |
| receivedEvent = YES; |
| }]; |
| |
| expect([command execute:nil]).notTo.beNil(); |
| expect([command.executing first]).will.equal(@YES); |
| |
| [subject sendNext:RACUnit.defaultUnit]; |
| [subject sendCompleted]; |
| |
| expect([command.executing first]).will.equal(@NO); |
| expect(receivedEvent).to.beFalsy(); |
| }); |
| |
| it(@"should send errors on the main thread", ^{ |
| RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(RACSignal *signal) { |
| return signal; |
| }]; |
| |
| NSError *error = [NSError errorWithDomain:@"" code:1 userInfo:nil]; |
| |
| __block RACScheduler *receivedScheduler = nil; |
| [command.errors subscribeNext:^(NSError *e) { |
| expect(e).to.equal(error); |
| receivedScheduler = RACScheduler.currentScheduler; |
| }]; |
| |
| RACSignal *errorSignal = [RACSignal error:error]; |
| |
| [[RACScheduler scheduler] schedule:^{ |
| expect([[command execute:errorSignal] waitUntilCompleted:NULL]).to.beTruthy(); |
| }]; |
| |
| expect(receivedScheduler).to.beNil(); |
| expect(receivedScheduler).will.equal(RACScheduler.mainThreadScheduler); |
| }); |
| |
| describe(@"enabled signal", ^{ |
| __block RACSubject *enabledSubject; |
| __block RACCommand *command; |
| |
| beforeEach(^{ |
| enabledSubject = [RACSubject subject]; |
| command = [[RACCommand alloc] initWithEnabled:enabledSubject signalBlock:^(id _) { |
| return [RACSignal return:RACUnit.defaultUnit]; |
| }]; |
| }); |
| |
| it(@"should send YES by default", ^{ |
| expect([command.enabled first]).to.equal(@YES); |
| }); |
| |
| it(@"should send whatever the enabledSignal has sent most recently", ^{ |
| [enabledSubject sendNext:@NO]; |
| expect([command.enabled first]).will.equal(@NO); |
| |
| [enabledSubject sendNext:@YES]; |
| expect([command.enabled first]).will.equal(@YES); |
| |
| [enabledSubject sendNext:@NO]; |
| expect([command.enabled first]).will.equal(@NO); |
| }); |
| |
| it(@"should sample enabledSignal synchronously at initialization time", ^{ |
| RACCommand *command = [[RACCommand alloc] initWithEnabled:[RACSignal return:@NO] signalBlock:^(id _) { |
| return [RACSignal empty]; |
| }]; |
| expect([command.enabled first]).to.equal(@NO); |
| }); |
| |
| it(@"should send NO while executing is YES and allowsConcurrentExecution is NO", ^{ |
| [[command.executionSignals flatten] subscribeNext:^(id _) { |
| expect([command.executing first]).to.equal(@YES); |
| expect([command.enabled first]).to.equal(@NO); |
| }]; |
| |
| expect([command.enabled first]).to.equal(@YES); |
| expect([[command execute:nil] asynchronouslyWaitUntilCompleted:NULL]).to.beTruthy(); |
| expect([command.enabled first]).to.equal(@YES); |
| }); |
| |
| it(@"should send YES while executing is YES and allowsConcurrentExecution is YES", ^{ |
| command.allowsConcurrentExecution = YES; |
| |
| __block BOOL outerExecuted = NO; |
| __block BOOL innerExecuted = NO; |
| |
| // Prevent infinite recursion by only responding to the first value. |
| [[[command.executionSignals |
| take:1] |
| flatten] |
| subscribeNext:^(id _) { |
| outerExecuted = YES; |
| |
| expect([command.executing first]).to.equal(@YES); |
| expect([command.enabled first]).to.equal(@YES); |
| |
| [[command execute:nil] subscribeCompleted:^{ |
| innerExecuted = YES; |
| }]; |
| }]; |
| |
| expect([command.enabled first]).to.equal(@YES); |
| |
| expect([command execute:nil]).notTo.beNil(); |
| expect(outerExecuted).will.beTruthy(); |
| expect(innerExecuted).will.beTruthy(); |
| |
| expect([command.enabled first]).to.equal(@YES); |
| }); |
| |
| it(@"should send an error from -execute: when NO", ^{ |
| [enabledSubject sendNext:@NO]; |
| |
| RACSignal *signal = [command execute:nil]; |
| expect(signal).notTo.beNil(); |
| |
| __block BOOL success = NO; |
| __block NSError *error = nil; |
| expect([signal firstOrDefault:nil success:&success error:&error]).to.beNil(); |
| expect(success).to.beFalsy(); |
| |
| expect(error).notTo.beNil(); |
| expect(error.domain).to.equal(RACCommandErrorDomain); |
| expect(error.code).to.equal(RACCommandErrorNotEnabled); |
| expect(error.userInfo[RACUnderlyingCommandErrorKey]).to.beIdenticalTo(command); |
| }); |
| |
| it(@"should always update on the main thread", ^{ |
| __block RACScheduler *updatedScheduler = nil; |
| [[command.enabled skip:1] subscribeNext:^(id _) { |
| updatedScheduler = RACScheduler.currentScheduler; |
| }]; |
| |
| [[RACScheduler scheduler] schedule:^{ |
| [enabledSubject sendNext:@NO]; |
| }]; |
| |
| expect([command.enabled first]).to.equal(@YES); |
| expect([command.enabled first]).will.equal(@NO); |
| expect(updatedScheduler).to.equal(RACScheduler.mainThreadScheduler); |
| }); |
| |
| it(@"should complete when the command is deallocated even if the input signal hasn't", ^{ |
| __block BOOL deallocated = NO; |
| __block BOOL completed = NO; |
| |
| @autoreleasepool { |
| RACCommand *command __attribute__((objc_precise_lifetime)) = [[RACCommand alloc] initWithEnabled:enabledSubject signalBlock:emptySignalBlock]; |
| [command.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ |
| deallocated = YES; |
| }]]; |
| |
| [command.enabled subscribeCompleted:^{ |
| completed = YES; |
| }]; |
| } |
| |
| expect(deallocated).will.beTruthy(); |
| expect(completed).will.beTruthy(); |
| }); |
| }); |
| |
| SpecEnd |