1<?php
2
3namespace React\Promise\PromiseTest;
4
5use React\Promise\Deferred;
6use React\Promise\UnhandledRejectionException;
7
8trait PromiseRejectedTestTrait
9{
10    /**
11     * @return \React\Promise\PromiseAdapter\PromiseAdapterInterface
12     */
13    abstract public function getPromiseTestAdapter(callable $canceller = null);
14
15    /** @test */
16    public function rejectedPromiseShouldBeImmutable()
17    {
18        $adapter = $this->getPromiseTestAdapter();
19
20        $mock = $this->createCallableMock();
21        $mock
22            ->expects($this->once())
23            ->method('__invoke')
24            ->with($this->identicalTo(1));
25
26        $adapter->reject(1);
27        $adapter->reject(2);
28
29        $adapter->promise()
30            ->then(
31                $this->expectCallableNever(),
32                $mock
33            );
34    }
35
36    /** @test */
37    public function rejectedPromiseShouldInvokeNewlyAddedCallback()
38    {
39        $adapter = $this->getPromiseTestAdapter();
40
41        $adapter->reject(1);
42
43        $mock = $this->createCallableMock();
44        $mock
45            ->expects($this->once())
46            ->method('__invoke')
47            ->with($this->identicalTo(1));
48
49        $adapter->promise()
50            ->then($this->expectCallableNever(), $mock);
51    }
52
53    /** @test */
54    public function shouldForwardUndefinedRejectionValue()
55    {
56        $adapter = $this->getPromiseTestAdapter();
57
58        $mock = $this->createCallableMock();
59        $mock
60            ->expects($this->once())
61            ->method('__invoke')
62            ->with(null);
63
64        $adapter->reject(1);
65        $adapter->promise()
66            ->then(
67                $this->expectCallableNever(),
68                function () {
69                    // Presence of rejection handler is enough to switch back
70                    // to resolve mode, even though it returns undefined.
71                    // The ONLY way to propagate a rejection is to re-throw or
72                    // return a rejected promise;
73                }
74            )
75            ->then(
76                $mock,
77                $this->expectCallableNever()
78            );
79    }
80
81    /** @test */
82    public function shouldSwitchFromErrbacksToCallbacksWhenErrbackDoesNotExplicitlyPropagate()
83    {
84        $adapter = $this->getPromiseTestAdapter();
85
86        $mock = $this->createCallableMock();
87        $mock
88            ->expects($this->once())
89            ->method('__invoke')
90            ->with($this->identicalTo(2));
91
92        $adapter->reject(1);
93        $adapter->promise()
94            ->then(
95                $this->expectCallableNever(),
96                function ($val) {
97                    return $val + 1;
98                }
99            )
100            ->then(
101                $mock,
102                $this->expectCallableNever()
103            );
104    }
105
106    /** @test */
107    public function shouldSwitchFromErrbacksToCallbacksWhenErrbackReturnsAResolution()
108    {
109        $adapter = $this->getPromiseTestAdapter();
110
111        $mock = $this->createCallableMock();
112        $mock
113            ->expects($this->once())
114            ->method('__invoke')
115            ->with($this->identicalTo(2));
116
117        $adapter->reject(1);
118        $adapter->promise()
119            ->then(
120                $this->expectCallableNever(),
121                function ($val) {
122                    return \React\Promise\resolve($val + 1);
123                }
124            )
125            ->then(
126                $mock,
127                $this->expectCallableNever()
128            );
129    }
130
131    /** @test */
132    public function shouldPropagateRejectionsWhenErrbackThrows()
133    {
134        $adapter = $this->getPromiseTestAdapter();
135
136        $exception = new \Exception();
137
138        $mock = $this->createCallableMock();
139        $mock
140            ->expects($this->once())
141            ->method('__invoke')
142            ->will($this->throwException($exception));
143
144        $mock2 = $this->createCallableMock();
145        $mock2
146            ->expects($this->once())
147            ->method('__invoke')
148            ->with($this->identicalTo($exception));
149
150        $adapter->reject(1);
151        $adapter->promise()
152            ->then(
153                $this->expectCallableNever(),
154                $mock
155            )
156            ->then(
157                $this->expectCallableNever(),
158                $mock2
159            );
160    }
161
162    /** @test */
163    public function shouldPropagateRejectionsWhenErrbackReturnsARejection()
164    {
165        $adapter = $this->getPromiseTestAdapter();
166
167        $mock = $this->createCallableMock();
168        $mock
169            ->expects($this->once())
170            ->method('__invoke')
171            ->with($this->identicalTo(2));
172
173        $adapter->reject(1);
174        $adapter->promise()
175            ->then(
176                $this->expectCallableNever(),
177                function ($val) {
178                    return \React\Promise\reject($val + 1);
179                }
180            )
181            ->then(
182                $this->expectCallableNever(),
183                $mock
184            );
185    }
186
187    /** @test */
188    public function doneShouldInvokeRejectionHandlerForRejectedPromise()
189    {
190        $adapter = $this->getPromiseTestAdapter();
191
192        $mock = $this->createCallableMock();
193        $mock
194            ->expects($this->once())
195            ->method('__invoke')
196            ->with($this->identicalTo(1));
197
198        $adapter->reject(1);
199        $this->assertNull($adapter->promise()->done(null, $mock));
200    }
201
202    /** @test */
203    public function doneShouldThrowExceptionThrownByRejectionHandlerForRejectedPromise()
204    {
205        $adapter = $this->getPromiseTestAdapter();
206
207        $this->setExpectedException('\Exception', 'UnhandledRejectionException');
208
209        $adapter->reject(1);
210        $this->assertNull($adapter->promise()->done(null, function () {
211            throw new \Exception('UnhandledRejectionException');
212        }));
213    }
214
215    /** @test */
216    public function doneShouldThrowUnhandledRejectionExceptionWhenRejectedWithNonExceptionForRejectedPromise()
217    {
218        $adapter = $this->getPromiseTestAdapter();
219
220        $this->setExpectedException('React\\Promise\\UnhandledRejectionException');
221
222        $adapter->reject(1);
223        $this->assertNull($adapter->promise()->done());
224    }
225
226    /** @test */
227    public function unhandledRejectionExceptionThrownByDoneHoldsRejectionValue()
228    {
229        $adapter = $this->getPromiseTestAdapter();
230
231        $expected = new \stdClass();
232
233        $adapter->reject($expected);
234
235        try {
236            $adapter->promise()->done();
237        } catch (UnhandledRejectionException $e) {
238            $this->assertSame($expected, $e->getReason());
239            return;
240        }
241
242        $this->fail();
243    }
244
245    /** @test */
246    public function doneShouldThrowUnhandledRejectionExceptionWhenRejectionHandlerRejectsForRejectedPromise()
247    {
248        $adapter = $this->getPromiseTestAdapter();
249
250        $this->setExpectedException('React\\Promise\\UnhandledRejectionException');
251
252        $adapter->reject(1);
253        $this->assertNull($adapter->promise()->done(null, function () {
254            return \React\Promise\reject();
255        }));
256    }
257
258    /** @test */
259    public function doneShouldThrowRejectionExceptionWhenRejectionHandlerRejectsWithExceptionForRejectedPromise()
260    {
261        $adapter = $this->getPromiseTestAdapter();
262
263        $this->setExpectedException('\Exception', 'UnhandledRejectionException');
264
265        $adapter->reject(1);
266        $this->assertNull($adapter->promise()->done(null, function () {
267            return \React\Promise\reject(new \Exception('UnhandledRejectionException'));
268        }));
269    }
270
271    /** @test */
272    public function doneShouldThrowExceptionProvidedAsRejectionValueForRejectedPromise()
273    {
274        $adapter = $this->getPromiseTestAdapter();
275
276        $this->setExpectedException('\Exception', 'UnhandledRejectionException');
277
278        $adapter->reject(new \Exception('UnhandledRejectionException'));
279        $this->assertNull($adapter->promise()->done());
280    }
281
282    /** @test */
283    public function doneShouldThrowWithDeepNestingPromiseChainsForRejectedPromise()
284    {
285        $this->setExpectedException('\Exception', 'UnhandledRejectionException');
286
287        $exception = new \Exception('UnhandledRejectionException');
288
289        $d = new Deferred();
290        $d->resolve();
291
292        $result = \React\Promise\resolve(\React\Promise\resolve($d->promise()->then(function () use ($exception) {
293            $d = new Deferred();
294            $d->resolve();
295
296            return \React\Promise\resolve($d->promise()->then(function () {}))->then(
297                function () use ($exception) {
298                    throw $exception;
299                }
300            );
301        })));
302
303        $result->done();
304    }
305
306    /** @test */
307    public function doneShouldRecoverWhenRejectionHandlerCatchesExceptionForRejectedPromise()
308    {
309        $adapter = $this->getPromiseTestAdapter();
310
311        $adapter->reject(new \Exception('UnhandledRejectionException'));
312        $this->assertNull($adapter->promise()->done(null, function (\Exception $e) {
313
314        }));
315    }
316
317    /** @test */
318    public function otherwiseShouldInvokeRejectionHandlerForRejectedPromise()
319    {
320        $adapter = $this->getPromiseTestAdapter();
321
322        $mock = $this->createCallableMock();
323        $mock
324            ->expects($this->once())
325            ->method('__invoke')
326            ->with($this->identicalTo(1));
327
328        $adapter->reject(1);
329        $adapter->promise()->otherwise($mock);
330    }
331
332    /** @test */
333    public function otherwiseShouldInvokeNonTypeHintedRejectionHandlerIfReasonIsAnExceptionForRejectedPromise()
334    {
335        $adapter = $this->getPromiseTestAdapter();
336
337        $exception = new \Exception();
338
339        $mock = $this->createCallableMock();
340        $mock
341            ->expects($this->once())
342            ->method('__invoke')
343            ->with($this->identicalTo($exception));
344
345        $adapter->reject($exception);
346        $adapter->promise()
347            ->otherwise(function ($reason) use ($mock) {
348                $mock($reason);
349            });
350    }
351
352    /** @test */
353    public function otherwiseShouldInvokeRejectionHandlerIfReasonMatchesTypehintForRejectedPromise()
354    {
355        $adapter = $this->getPromiseTestAdapter();
356
357        $exception = new \InvalidArgumentException();
358
359        $mock = $this->createCallableMock();
360        $mock
361            ->expects($this->once())
362            ->method('__invoke')
363            ->with($this->identicalTo($exception));
364
365        $adapter->reject($exception);
366        $adapter->promise()
367            ->otherwise(function (\InvalidArgumentException $reason) use ($mock) {
368                $mock($reason);
369            });
370    }
371
372    /** @test */
373    public function otherwiseShouldNotInvokeRejectionHandlerIfReaonsDoesNotMatchTypehintForRejectedPromise()
374    {
375        $adapter = $this->getPromiseTestAdapter();
376
377        $exception = new \Exception();
378
379        $mock = $this->expectCallableNever();
380
381        $adapter->reject($exception);
382        $adapter->promise()
383            ->otherwise(function (\InvalidArgumentException $reason) use ($mock) {
384                $mock($reason);
385            });
386    }
387
388    /** @test */
389    public function alwaysShouldNotSuppressRejectionForRejectedPromise()
390    {
391        $adapter = $this->getPromiseTestAdapter();
392
393        $exception = new \Exception();
394
395        $mock = $this->createCallableMock();
396        $mock
397            ->expects($this->once())
398            ->method('__invoke')
399            ->with($this->identicalTo($exception));
400
401        $adapter->reject($exception);
402        $adapter->promise()
403            ->always(function () {})
404            ->then(null, $mock);
405    }
406
407    /** @test */
408    public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsANonPromiseForRejectedPromise()
409    {
410        $adapter = $this->getPromiseTestAdapter();
411
412        $exception = new \Exception();
413
414        $mock = $this->createCallableMock();
415        $mock
416            ->expects($this->once())
417            ->method('__invoke')
418            ->with($this->identicalTo($exception));
419
420        $adapter->reject($exception);
421        $adapter->promise()
422            ->always(function () {
423                return 1;
424            })
425            ->then(null, $mock);
426    }
427
428    /** @test */
429    public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsAPromiseForRejectedPromise()
430    {
431        $adapter = $this->getPromiseTestAdapter();
432
433        $exception = new \Exception();
434
435        $mock = $this->createCallableMock();
436        $mock
437            ->expects($this->once())
438            ->method('__invoke')
439            ->with($this->identicalTo($exception));
440
441        $adapter->reject($exception);
442        $adapter->promise()
443            ->always(function () {
444                return \React\Promise\resolve(1);
445            })
446            ->then(null, $mock);
447    }
448
449    /** @test */
450    public function alwaysShouldRejectWhenHandlerThrowsForRejectedPromise()
451    {
452        $adapter = $this->getPromiseTestAdapter();
453
454        $exception1 = new \Exception();
455        $exception2 = new \Exception();
456
457        $mock = $this->createCallableMock();
458        $mock
459            ->expects($this->once())
460            ->method('__invoke')
461            ->with($this->identicalTo($exception2));
462
463        $adapter->reject($exception1);
464        $adapter->promise()
465            ->always(function () use ($exception2) {
466                throw $exception2;
467            })
468            ->then(null, $mock);
469    }
470
471    /** @test */
472    public function alwaysShouldRejectWhenHandlerRejectsForRejectedPromise()
473    {
474        $adapter = $this->getPromiseTestAdapter();
475
476        $exception1 = new \Exception();
477        $exception2 = new \Exception();
478
479        $mock = $this->createCallableMock();
480        $mock
481            ->expects($this->once())
482            ->method('__invoke')
483            ->with($this->identicalTo($exception2));
484
485        $adapter->reject($exception1);
486        $adapter->promise()
487            ->always(function () use ($exception2) {
488                return \React\Promise\reject($exception2);
489            })
490            ->then(null, $mock);
491    }
492
493    /** @test */
494    public function cancelShouldReturnNullForRejectedPromise()
495    {
496        $adapter = $this->getPromiseTestAdapter();
497
498        $adapter->reject();
499
500        $this->assertNull($adapter->promise()->cancel());
501    }
502
503    /** @test */
504    public function cancelShouldHaveNoEffectForRejectedPromise()
505    {
506        $adapter = $this->getPromiseTestAdapter($this->expectCallableNever());
507
508        $adapter->reject();
509
510        $adapter->promise()->cancel();
511    }
512}
513