RACCommandSpec.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. //
  2. // RACCommandSpec.m
  3. // ReactiveCocoa
  4. //
  5. // Created by Josh Abernathy on 8/31/12.
  6. // Copyright (c) 2012 GitHub, Inc. All rights reserved.
  7. //
  8. #import <Quick/Quick.h>
  9. #import <Nimble/Nimble.h>
  10. #import "NSArray+RACSequenceAdditions.h"
  11. #import "NSObject+RACDeallocating.h"
  12. #import "NSObject+RACPropertySubscribing.h"
  13. #import "RACCommand.h"
  14. #import "RACCompoundDisposable.h"
  15. #import "RACDisposable.h"
  16. #import "RACEvent.h"
  17. #import "RACScheduler.h"
  18. #import "RACSequence.h"
  19. #import "RACSignal+Operations.h"
  20. #import "RACSubject.h"
  21. #import "RACUnit.h"
  22. QuickSpecBegin(RACCommandSpec)
  23. RACSignal * (^emptySignalBlock)(id) = ^(id _) {
  24. return [RACSignal empty];
  25. };
  26. qck_describe(@"with a simple signal block", ^{
  27. __block RACCommand *command;
  28. qck_beforeEach(^{
  29. command = [[RACCommand alloc] initWithSignalBlock:^(id value) {
  30. return [RACSignal return:value];
  31. }];
  32. expect(command).notTo(beNil());
  33. expect(@(command.allowsConcurrentExecution)).to(beFalsy());
  34. });
  35. qck_it(@"should be enabled by default", ^{
  36. expect([command.enabled first]).to(equal(@YES));
  37. });
  38. qck_it(@"should not be executing by default", ^{
  39. expect([command.executing first]).to(equal(@NO));
  40. });
  41. qck_it(@"should create an execution signal", ^{
  42. __block NSUInteger signalsReceived = 0;
  43. __block BOOL completed = NO;
  44. id value = NSNull.null;
  45. [command.executionSignals subscribeNext:^(RACSignal *signal) {
  46. signalsReceived++;
  47. [signal subscribeNext:^(id x) {
  48. expect(x).to(equal(value));
  49. } completed:^{
  50. completed = YES;
  51. }];
  52. }];
  53. expect(@(signalsReceived)).to(equal(@0));
  54. [command execute:value];
  55. expect(@(signalsReceived)).toEventually(equal(@1));
  56. expect(@(completed)).to(beTruthy());
  57. });
  58. qck_it(@"should return the execution signal from -execute:", ^{
  59. __block BOOL completed = NO;
  60. id value = NSNull.null;
  61. [[command
  62. execute:value]
  63. subscribeNext:^(id x) {
  64. expect(x).to(equal(value));
  65. } completed:^{
  66. completed = YES;
  67. }];
  68. expect(@(completed)).toEventually(beTruthy());
  69. });
  70. qck_it(@"should always send executionSignals on the main thread", ^{
  71. __block RACScheduler *receivedScheduler = nil;
  72. [command.executionSignals subscribeNext:^(id _) {
  73. receivedScheduler = RACScheduler.currentScheduler;
  74. }];
  75. [[RACScheduler scheduler] schedule:^{
  76. expect(@([[command execute:nil] waitUntilCompleted:NULL])).to(beTruthy());
  77. }];
  78. expect(receivedScheduler).to(beNil());
  79. expect(receivedScheduler).toEventually(equal(RACScheduler.mainThreadScheduler));
  80. });
  81. qck_it(@"should not send anything on 'errors' by default", ^{
  82. __block BOOL receivedError = NO;
  83. [command.errors subscribeNext:^(id _) {
  84. receivedError = YES;
  85. }];
  86. expect(@([[command execute:nil] asynchronouslyWaitUntilCompleted:NULL])).to(beTruthy());
  87. expect(@(receivedError)).to(beFalsy());
  88. });
  89. qck_it(@"should be executing while an execution signal is running", ^{
  90. [command.executionSignals subscribeNext:^(RACSignal *signal) {
  91. [signal subscribeNext:^(id x) {
  92. expect([command.executing first]).to(equal(@YES));
  93. }];
  94. }];
  95. expect(@([[command execute:nil] asynchronouslyWaitUntilCompleted:NULL])).to(beTruthy());
  96. expect([command.executing first]).to(equal(@NO));
  97. });
  98. qck_it(@"should always update executing on the main thread", ^{
  99. __block RACScheduler *updatedScheduler = nil;
  100. [[command.executing skip:1] subscribeNext:^(NSNumber *executing) {
  101. if (!executing.boolValue) return;
  102. updatedScheduler = RACScheduler.currentScheduler;
  103. }];
  104. [[RACScheduler scheduler] schedule:^{
  105. expect(@([[command execute:nil] waitUntilCompleted:NULL])).to(beTruthy());
  106. }];
  107. expect([command.executing first]).to(equal(@NO));
  108. expect(updatedScheduler).toEventually(equal(RACScheduler.mainThreadScheduler));
  109. });
  110. qck_it(@"should dealloc without subscribers", ^{
  111. __block BOOL disposed = NO;
  112. @autoreleasepool {
  113. RACCommand *command __attribute__((objc_precise_lifetime)) = [[RACCommand alloc] initWithSignalBlock:emptySignalBlock];
  114. [command.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
  115. disposed = YES;
  116. }]];
  117. }
  118. expect(@(disposed)).toEventually(beTruthy());
  119. });
  120. qck_it(@"should complete signals on the main thread when deallocated", ^{
  121. __block RACScheduler *executionSignalsScheduler = nil;
  122. __block RACScheduler *executingScheduler = nil;
  123. __block RACScheduler *enabledScheduler = nil;
  124. __block RACScheduler *errorsScheduler = nil;
  125. [[RACScheduler scheduler] schedule:^{
  126. @autoreleasepool {
  127. RACCommand *command __attribute__((objc_precise_lifetime)) = [[RACCommand alloc] initWithSignalBlock:emptySignalBlock];
  128. [command.executionSignals subscribeCompleted:^{
  129. executionSignalsScheduler = RACScheduler.currentScheduler;
  130. }];
  131. [command.executing subscribeCompleted:^{
  132. executingScheduler = RACScheduler.currentScheduler;
  133. }];
  134. [command.enabled subscribeCompleted:^{
  135. enabledScheduler = RACScheduler.currentScheduler;
  136. }];
  137. [command.errors subscribeCompleted:^{
  138. errorsScheduler = RACScheduler.currentScheduler;
  139. }];
  140. }
  141. }];
  142. expect(executionSignalsScheduler).toEventually(equal(RACScheduler.mainThreadScheduler));
  143. expect(executingScheduler).toEventually(equal(RACScheduler.mainThreadScheduler));
  144. expect(enabledScheduler).toEventually(equal(RACScheduler.mainThreadScheduler));
  145. expect(errorsScheduler).toEventually(equal(RACScheduler.mainThreadScheduler));
  146. });
  147. });
  148. qck_it(@"should invoke the signalBlock once per execution", ^{
  149. NSMutableArray *valuesReceived = [NSMutableArray array];
  150. RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(id x) {
  151. [valuesReceived addObject:x];
  152. return [RACSignal empty];
  153. }];
  154. expect(@([[command execute:@"foo"] asynchronouslyWaitUntilCompleted:NULL])).to(beTruthy());
  155. expect(valuesReceived).to(equal((@[ @"foo" ])));
  156. expect(@([[command execute:@"bar"] asynchronouslyWaitUntilCompleted:NULL])).to(beTruthy());
  157. expect(valuesReceived).to(equal((@[ @"foo", @"bar" ])));
  158. });
  159. qck_it(@"should send on executionSignals in order of execution", ^{
  160. RACCommand<RACSequence *> *command = [[RACCommand alloc] initWithSignalBlock:^(RACSequence *seq) {
  161. return [seq signalWithScheduler:RACScheduler.immediateScheduler];
  162. }];
  163. NSMutableArray *valuesReceived = [NSMutableArray array];
  164. [[command.executionSignals
  165. concat]
  166. subscribeNext:^(id x) {
  167. [valuesReceived addObject:x];
  168. }];
  169. RACSequence *first = @[ @"foo", @"bar" ].rac_sequence;
  170. expect(@([[command execute:first] asynchronouslyWaitUntilCompleted:NULL])).to(beTruthy());
  171. RACSequence *second = @[ @"buzz", @"baz" ].rac_sequence;
  172. expect(@([[command execute:second] asynchronouslyWaitUntilCompleted:NULL])).to(beTruthy());
  173. NSArray *expectedValues = @[ @"foo", @"bar", @"buzz", @"baz" ];
  174. expect(valuesReceived).to(equal(expectedValues));
  175. });
  176. qck_it(@"should wait for all signals to complete or error before executing sends NO", ^{
  177. RACCommand<RACSignal *> *command = [[RACCommand alloc] initWithSignalBlock:^(RACSignal *signal) {
  178. return signal;
  179. }];
  180. command.allowsConcurrentExecution = YES;
  181. RACSubject *firstSubject = [RACSubject subject];
  182. expect([command execute:firstSubject]).notTo(beNil());
  183. RACSubject *secondSubject = [RACSubject subject];
  184. expect([command execute:secondSubject]).notTo(beNil());
  185. expect([command.executing first]).toEventually(equal(@YES));
  186. [firstSubject sendError:nil];
  187. expect([command.executing first]).to(equal(@YES));
  188. [secondSubject sendNext:nil];
  189. expect([command.executing first]).to(equal(@YES));
  190. [secondSubject sendCompleted];
  191. expect([command.executing first]).toEventually(equal(@NO));
  192. });
  193. qck_it(@"should have allowsConcurrentExecution be observable", ^{
  194. RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(RACSignal *signal) {
  195. return signal;
  196. }];
  197. RACSubject *completion = [RACSubject subject];
  198. RACSignal *allowsConcurrentExecution = [[RACObserve(command, allowsConcurrentExecution)
  199. takeUntil:completion]
  200. replayLast];
  201. command.allowsConcurrentExecution = YES;
  202. expect([allowsConcurrentExecution first]).to(beTrue());
  203. [completion sendCompleted];
  204. });
  205. qck_it(@"should not deliver errors from executionSignals", ^{
  206. RACSubject *subject = [RACSubject subject];
  207. NSMutableArray *receivedEvents = [NSMutableArray array];
  208. RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
  209. return subject;
  210. }];
  211. [[[command.executionSignals
  212. flatten]
  213. materialize]
  214. subscribeNext:^(RACEvent *event) {
  215. [receivedEvents addObject:event];
  216. }];
  217. expect([command execute:nil]).notTo(beNil());
  218. expect([command.executing first]).toEventually(equal(@YES));
  219. [subject sendNext:RACUnit.defaultUnit];
  220. NSArray *expectedEvents = @[ [RACEvent eventWithValue:RACUnit.defaultUnit] ];
  221. expect(receivedEvents).toEventually(equal(expectedEvents));
  222. expect([command.executing first]).to(equal(@YES));
  223. [subject sendNext:@"foo"];
  224. expectedEvents = @[ [RACEvent eventWithValue:RACUnit.defaultUnit], [RACEvent eventWithValue:@"foo"] ];
  225. expect(receivedEvents).toEventually(equal(expectedEvents));
  226. expect([command.executing first]).to(equal(@YES));
  227. NSError *error = [NSError errorWithDomain:@"" code:1 userInfo:nil];
  228. [subject sendError:error];
  229. expect([command.executing first]).toEventually(equal(@NO));
  230. expect(receivedEvents).to(equal(expectedEvents));
  231. });
  232. qck_it(@"should deliver errors from -execute:", ^{
  233. RACSubject *subject = [RACSubject subject];
  234. NSMutableArray *receivedEvents = [NSMutableArray array];
  235. RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
  236. return subject;
  237. }];
  238. [[[command
  239. execute:nil]
  240. materialize]
  241. subscribeNext:^(RACEvent *event) {
  242. [receivedEvents addObject:event];
  243. }];
  244. expect([command.executing first]).toEventually(equal(@YES));
  245. [subject sendNext:RACUnit.defaultUnit];
  246. NSArray *expectedEvents = @[ [RACEvent eventWithValue:RACUnit.defaultUnit] ];
  247. expect(receivedEvents).toEventually(equal(expectedEvents));
  248. expect([command.executing first]).to(equal(@YES));
  249. [subject sendNext:@"foo"];
  250. expectedEvents = @[ [RACEvent eventWithValue:RACUnit.defaultUnit], [RACEvent eventWithValue:@"foo"] ];
  251. expect(receivedEvents).toEventually(equal(expectedEvents));
  252. expect([command.executing first]).to(equal(@YES));
  253. NSError *error = [NSError errorWithDomain:@"" code:1 userInfo:nil];
  254. [subject sendError:error];
  255. expectedEvents = @[ [RACEvent eventWithValue:RACUnit.defaultUnit], [RACEvent eventWithValue:@"foo"], [RACEvent eventWithError:error] ];
  256. expect(receivedEvents).toEventually(equal(expectedEvents));
  257. expect([command.executing first]).toEventually(equal(@NO));
  258. });
  259. qck_it(@"should deliver errors onto 'errors'", ^{
  260. RACCommand<RACSignal *> *command = [[RACCommand alloc] initWithSignalBlock:^(RACSignal *signal) {
  261. return signal;
  262. }];
  263. command.allowsConcurrentExecution = YES;
  264. RACSubject *firstSubject = [RACSubject subject];
  265. expect([command execute:firstSubject]).notTo(beNil());
  266. RACSubject *secondSubject = [RACSubject subject];
  267. expect([command execute:secondSubject]).notTo(beNil());
  268. NSError *firstError = [NSError errorWithDomain:@"" code:1 userInfo:nil];
  269. NSError *secondError = [NSError errorWithDomain:@"" code:2 userInfo:nil];
  270. // We should receive errors from our previously-started executions.
  271. NSMutableArray *receivedErrors = [NSMutableArray array];
  272. [command.errors subscribeNext:^(NSError *error) {
  273. [receivedErrors addObject:error];
  274. }];
  275. expect([command.executing first]).toEventually(equal(@YES));
  276. [firstSubject sendError:firstError];
  277. expect([command.executing first]).toEventually(equal(@YES));
  278. NSArray *expected = @[ firstError ];
  279. expect(receivedErrors).toEventually(equal(expected));
  280. [secondSubject sendError:secondError];
  281. expect([command.executing first]).toEventually(equal(@NO));
  282. expected = @[ firstError, secondError ];
  283. expect(receivedErrors).toEventually(equal(expected));
  284. });
  285. qck_it(@"should not deliver non-error events onto 'errors'", ^{
  286. RACSubject *subject = [RACSubject subject];
  287. RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
  288. return subject;
  289. }];
  290. __block BOOL receivedEvent = NO;
  291. [command.errors subscribeNext:^(id _) {
  292. receivedEvent = YES;
  293. }];
  294. expect([command execute:nil]).notTo(beNil());
  295. expect([command.executing first]).toEventually(equal(@YES));
  296. [subject sendNext:RACUnit.defaultUnit];
  297. [subject sendCompleted];
  298. expect([command.executing first]).toEventually(equal(@NO));
  299. expect(@(receivedEvent)).to(beFalsy());
  300. });
  301. qck_it(@"should send errors on the main thread", ^{
  302. RACCommand<RACSignal *> *command = [[RACCommand alloc] initWithSignalBlock:^(RACSignal *signal) {
  303. return signal;
  304. }];
  305. NSError *error = [NSError errorWithDomain:@"" code:1 userInfo:nil];
  306. __block RACScheduler *receivedScheduler = nil;
  307. [command.errors subscribeNext:^(NSError *e) {
  308. expect(e).to(equal(error));
  309. receivedScheduler = RACScheduler.currentScheduler;
  310. }];
  311. RACSignal *errorSignal = [RACSignal error:error];
  312. [[RACScheduler scheduler] schedule:^{
  313. [command execute:errorSignal];
  314. }];
  315. expect(receivedScheduler).to(beNil());
  316. expect(receivedScheduler).toEventually(equal(RACScheduler.mainThreadScheduler));
  317. });
  318. qck_describe(@"enabled signal", ^{
  319. __block RACSubject *enabledSubject;
  320. __block RACCommand *command;
  321. qck_beforeEach(^{
  322. enabledSubject = [RACSubject subject];
  323. command = [[RACCommand alloc] initWithEnabled:enabledSubject signalBlock:^(id _) {
  324. return [RACSignal return:RACUnit.defaultUnit];
  325. }];
  326. });
  327. qck_it(@"should send YES by default", ^{
  328. expect([command.enabled first]).to(equal(@YES));
  329. });
  330. qck_it(@"should send whatever the enabledSignal has sent most recently", ^{
  331. [enabledSubject sendNext:@NO];
  332. expect([command.enabled first]).toEventually(equal(@NO));
  333. [enabledSubject sendNext:@YES];
  334. expect([command.enabled first]).toEventually(equal(@YES));
  335. [enabledSubject sendNext:@NO];
  336. expect([command.enabled first]).toEventually(equal(@NO));
  337. });
  338. qck_it(@"should sample enabledSignal synchronously at initialization time", ^{
  339. RACCommand *command = [[RACCommand alloc] initWithEnabled:[RACSignal return:@NO] signalBlock:^(id _) {
  340. return [RACSignal empty];
  341. }];
  342. expect([command.enabled first]).to(equal(@NO));
  343. });
  344. qck_it(@"should send NO while executing is YES and allowsConcurrentExecution is NO", ^{
  345. [[command.executionSignals flatten] subscribeNext:^(id _) {
  346. expect([command.executing first]).to(equal(@YES));
  347. expect([command.enabled first]).to(equal(@NO));
  348. }];
  349. expect([command.enabled first]).to(equal(@YES));
  350. expect(@([[command execute:nil] asynchronouslyWaitUntilCompleted:NULL])).to(beTruthy());
  351. expect([command.enabled first]).to(equal(@YES));
  352. });
  353. qck_it(@"should send YES while executing is YES and allowsConcurrentExecution is YES", ^{
  354. command.allowsConcurrentExecution = YES;
  355. __block BOOL outerExecuted = NO;
  356. __block BOOL innerExecuted = NO;
  357. // Prevent infinite recursion by only responding to the first value.
  358. [[[command.executionSignals
  359. take:1]
  360. flatten]
  361. subscribeNext:^(id _) {
  362. outerExecuted = YES;
  363. expect([command.executing first]).to(equal(@YES));
  364. expect([command.enabled first]).to(equal(@YES));
  365. [[command execute:nil] subscribeCompleted:^{
  366. innerExecuted = YES;
  367. }];
  368. }];
  369. expect([command.enabled first]).to(equal(@YES));
  370. expect([command execute:nil]).notTo(beNil());
  371. expect(@(outerExecuted)).toEventually(beTruthy());
  372. expect(@(innerExecuted)).toEventually(beTruthy());
  373. expect([command.enabled first]).to(equal(@YES));
  374. });
  375. qck_it(@"should send an error from -execute: when NO", ^{
  376. [enabledSubject sendNext:@NO];
  377. RACSignal *signal = [command execute:nil];
  378. expect(signal).notTo(beNil());
  379. __block BOOL success = NO;
  380. __block NSError *error = nil;
  381. expect([signal firstOrDefault:nil success:&success error:&error]).to(beNil());
  382. expect(@(success)).to(beFalsy());
  383. expect(error).notTo(beNil());
  384. expect(error.domain).to(equal(RACCommandErrorDomain));
  385. expect(@(error.code)).to(equal(@(RACCommandErrorNotEnabled)));
  386. expect(error.userInfo[RACUnderlyingCommandErrorKey]).to(beIdenticalTo(command));
  387. });
  388. qck_it(@"should always update on the main thread", ^{
  389. __block RACScheduler *updatedScheduler = nil;
  390. [[command.enabled skip:1] subscribeNext:^(id _) {
  391. updatedScheduler = RACScheduler.currentScheduler;
  392. }];
  393. [[RACScheduler scheduler] schedule:^{
  394. [enabledSubject sendNext:@NO];
  395. }];
  396. expect([command.enabled first]).to(equal(@YES));
  397. expect([command.enabled first]).toEventually(equal(@NO));
  398. expect(updatedScheduler).to(equal(RACScheduler.mainThreadScheduler));
  399. });
  400. qck_it(@"should complete when the command is deallocated even if the input signal hasn't", ^{
  401. __block BOOL deallocated = NO;
  402. __block BOOL completed = NO;
  403. @autoreleasepool {
  404. RACCommand *command __attribute__((objc_precise_lifetime)) = [[RACCommand alloc] initWithEnabled:enabledSubject signalBlock:emptySignalBlock];
  405. [command.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
  406. deallocated = YES;
  407. }]];
  408. [command.enabled subscribeCompleted:^{
  409. completed = YES;
  410. }];
  411. }
  412. expect(@(deallocated)).toEventually(beTruthy());
  413. expect(@(completed)).toEventually(beTruthy());
  414. });
  415. });
  416. QuickSpecEnd