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