|
| 1 | +test(() => { |
| 2 | + const source = new Observable(subscriber => { |
| 3 | + subscriber.next(1); |
| 4 | + subscriber.next(2); |
| 5 | + subscriber.next(3); |
| 6 | + subscriber.complete(); |
| 7 | + }); |
| 8 | + |
| 9 | + const caughtObservable = source.catch(() => { |
| 10 | + assert_unreached("catch() is not called"); |
| 11 | + }); |
| 12 | + |
| 13 | + const results = []; |
| 14 | + |
| 15 | + caughtObservable.subscribe({ |
| 16 | + next: value => results.push(value), |
| 17 | + complete: () => results.push('complete') |
| 18 | + }); |
| 19 | + |
| 20 | + assert_array_equals(results, [1, 2, 3, 'complete']); |
| 21 | +}, "catch(): Returns an Observable that is a pass-through for next()/complete()"); |
| 22 | + |
| 23 | +test(() => { |
| 24 | + let sourceError = new Error("from the source"); |
| 25 | + const source = new Observable(subscriber => { |
| 26 | + subscriber.next(1); |
| 27 | + subscriber.next(2); |
| 28 | + subscriber.error(sourceError); |
| 29 | + }); |
| 30 | + |
| 31 | + const caughtObservable = source.catch(error => { |
| 32 | + assert_equals(error, sourceError); |
| 33 | + return new Observable(subscriber => { |
| 34 | + subscriber.next(3); |
| 35 | + subscriber.complete(); |
| 36 | + }); |
| 37 | + }); |
| 38 | + |
| 39 | + const results = []; |
| 40 | + |
| 41 | + caughtObservable.subscribe({ |
| 42 | + next: value => results.push(value), |
| 43 | + complete: () => results.push("complete"), |
| 44 | + }); |
| 45 | + |
| 46 | + assert_array_equals(results, [1, 2, 3, 'complete']); |
| 47 | +}, "catch(): Handle errors from source and flatten to a new Observable"); |
| 48 | + |
| 49 | +test(() => { |
| 50 | + const sourceError = new Error("from the source"); |
| 51 | + const source = new Observable(subscriber => { |
| 52 | + subscriber.next(1); |
| 53 | + subscriber.next(2); |
| 54 | + subscriber.error(sourceError); |
| 55 | + }); |
| 56 | + |
| 57 | + const catchCallbackError = new Error("from the catch callback"); |
| 58 | + const caughtObservable = source.catch(error => { |
| 59 | + assert_equals(error, sourceError); |
| 60 | + throw catchCallbackError; |
| 61 | + }); |
| 62 | + |
| 63 | + const results = []; |
| 64 | + |
| 65 | + caughtObservable.subscribe({ |
| 66 | + next: value => results.push(value), |
| 67 | + error: error => { |
| 68 | + results.push(error); |
| 69 | + }, |
| 70 | + complete: () => results.push('complete'), |
| 71 | + }); |
| 72 | + |
| 73 | + assert_array_equals(results, [1, 2, catchCallbackError]); |
| 74 | +}, "catch(): Errors thrown in the catch() callback are sent to the consumer's error handler"); |
| 75 | + |
| 76 | +test(() => { |
| 77 | + // A common use case is logging and keeping the stream alive. |
| 78 | + const source = new Observable(subscriber => { |
| 79 | + subscriber.next(1); |
| 80 | + subscriber.next(2); |
| 81 | + subscriber.next(3); |
| 82 | + subscriber.complete(); |
| 83 | + }); |
| 84 | + |
| 85 | + const flatteningError = new Error("from the flattening operation"); |
| 86 | + function errorsOnTwo(value) { |
| 87 | + return new Observable(subscriber => { |
| 88 | + if (value === 2) { |
| 89 | + subscriber.error(flatteningError); |
| 90 | + } else { |
| 91 | + subscriber.next(value); |
| 92 | + subscriber.complete(); |
| 93 | + } |
| 94 | + }); |
| 95 | + } |
| 96 | + |
| 97 | + const results = []; |
| 98 | + |
| 99 | + source.flatMap(value => errorsOnTwo(value) |
| 100 | + .catch(error => { |
| 101 | + results.push(error); |
| 102 | + // This empty array converts to an Observable which automatically |
| 103 | + // completes. |
| 104 | + return []; |
| 105 | + }) |
| 106 | + ).subscribe({ |
| 107 | + next: value => results.push(value), |
| 108 | + complete: () => results.push("complete") |
| 109 | + }); |
| 110 | + |
| 111 | + assert_array_equals(results, [1, flatteningError, 3, "complete"]); |
| 112 | +}, "catch(): CatchHandler can return an empty iterable"); |
| 113 | + |
| 114 | +promise_test(async () => { |
| 115 | + const sourceError = new Error("from the source"); |
| 116 | + const source = new Observable(subscriber => { |
| 117 | + subscriber.next(1); |
| 118 | + subscriber.next(2); |
| 119 | + subscriber.error(sourceError); |
| 120 | + }); |
| 121 | + |
| 122 | + const caughtObservable = source.catch(error => { |
| 123 | + assert_equals(error, sourceError); |
| 124 | + return Promise.resolve(error.message); |
| 125 | + }); |
| 126 | + |
| 127 | + const results = await caughtObservable.toArray(); |
| 128 | + |
| 129 | + assert_array_equals(results, [1, 2, "from the source"]); |
| 130 | +}, "catch(): CatchHandler can return a Promise"); |
| 131 | + |
| 132 | +promise_test(async () => { |
| 133 | + const source = new Observable(subscriber => { |
| 134 | + subscriber.next(1); |
| 135 | + subscriber.next(2); |
| 136 | + subscriber.error(new Error('from the source')); |
| 137 | + }); |
| 138 | + |
| 139 | + const caughtObservable = source.catch(async function* (error) { |
| 140 | + assert_true(error instanceof Error); |
| 141 | + assert_equals(error.message, 'from the source'); |
| 142 | + yield 3; |
| 143 | + }); |
| 144 | + |
| 145 | + const results = await caughtObservable.toArray(); |
| 146 | + |
| 147 | + assert_array_equals(results, [1, 2, 3], 'catch(): should handle returning an observable'); |
| 148 | +}, 'catch(): should handle returning an async iterable'); |
| 149 | + |
| 150 | +test(() => { |
| 151 | + const sourceError = new Error("from the source"); |
| 152 | + const source = new Observable(subscriber => { |
| 153 | + subscriber.next(1); |
| 154 | + subscriber.next(2); |
| 155 | + subscriber.error(sourceError); |
| 156 | + }); |
| 157 | + |
| 158 | + const caughtObservable = source.catch(error => { |
| 159 | + assert_equals(error, sourceError); |
| 160 | + // Primitive values like this are not convertible to an Observable, via the |
| 161 | + // `from()` semantics. |
| 162 | + return 3; |
| 163 | + }); |
| 164 | + |
| 165 | + const results = []; |
| 166 | + |
| 167 | + caughtObservable.subscribe({ |
| 168 | + next: value => results.push(value), |
| 169 | + error: error => { |
| 170 | + assert_true(error instanceof TypeError); |
| 171 | + results.push("TypeError"); |
| 172 | + }, |
| 173 | + complete: () => results.push("complete"), |
| 174 | + }); |
| 175 | + |
| 176 | + assert_array_equals(results, [1, 2, "TypeError"]); |
| 177 | +}, "catch(): CatchHandler emits an error if the value returned is not " + |
| 178 | + "convertible to an Observable"); |
| 179 | + |
| 180 | +test(() => { |
| 181 | + const source = new Observable(subscriber => { |
| 182 | + susbcriber.error(new Error("from the source")); |
| 183 | + }); |
| 184 | + |
| 185 | + const results = []; |
| 186 | + |
| 187 | + const innerSubscriptionError = new Error("CatchHandler subscription error"); |
| 188 | + const catchObservable = source.catch(() => { |
| 189 | + results.push('CatchHandler invoked'); |
| 190 | + return new Observable(subscriber => { |
| 191 | + throw innerSubscriptionError; |
| 192 | + }); |
| 193 | + }); |
| 194 | + |
| 195 | + catchObservable.subscribe({ |
| 196 | + error: e => { |
| 197 | + results.push(e); |
| 198 | + } |
| 199 | + }); |
| 200 | + |
| 201 | + assert_array_equals(results, ['CatchHandler invoked', innerSubscriptionError]); |
| 202 | +}, "catch(): CatchHandler returns an Observable that throws immediately on " + |
| 203 | + "subscription"); |
| 204 | + |
| 205 | +// This test asserts that the relationship between (a) the AbortSignal passed |
| 206 | +// into `subscribe()` and (b) the AbortSignal associated with the Observable |
| 207 | +// returned from `catch()`'s CatchHandler is not a "dependent" relationship. |
| 208 | +// This is important because Observables have moved away from the "dependent |
| 209 | +// abort signal" infrastructure in https://github.com/WICG/observable/pull/154, |
| 210 | +// and this test asserts so. |
| 211 | +// |
| 212 | +// Here are all of the associated Observables and signals in this test: |
| 213 | +// 1. Raw outer signal passed into `subscribe()` |
| 214 | +// 2. catchObservable's inner Subscriber's signal |
| 215 | +// a. Per the above PR, and Subscriber's initialization logic [1], this |
| 216 | +// signal is set to abort in response to (1)'s abort algorithms. This |
| 217 | +// means its "abort" event gets fired before (1)'s. |
| 218 | +// 3. Inner CatchHandler-returned Observable's Subscriber's signal |
| 219 | +// a. Also per [1], this is set to abort in response to (2)'s abort |
| 220 | +// algorithms, since we subscribe to this "inner Observable" with (2)'s |
| 221 | +// signal as the `SubscribeOptions#signal`. |
| 222 | +// |
| 223 | +// (1), (2), and (3) above all form an abort chain: |
| 224 | +// (1) --> (2) --> (3) |
| 225 | +// |
| 226 | +// …such that when (1) aborts, its abort algorithms immediately abort (2), |
| 227 | +// whose abort algorithms immediately abort (3). Finally on the way back up the |
| 228 | +// chain, (3)'s `abort` event is fired, (2)'s `abort` event is fired, and then |
| 229 | +// (1)'s `abort` event is fired. This ordering of abort events is what this test |
| 230 | +// ensures. |
| 231 | +// |
| 232 | +// [1]: https://wicg.github.io/observable/#ref-for-abortsignal-add |
| 233 | +test(() => { |
| 234 | + const results = []; |
| 235 | + const source = new Observable(subscriber => |
| 236 | + susbcriber.error(new Error("from the source"))); |
| 237 | + |
| 238 | + const catchObservable = source.catch(() => { |
| 239 | + return new Observable(subscriber => { |
| 240 | + subscriber.addTeardown(() => results.push('inner teardown')); |
| 241 | + subscriber.signal.addEventListener('abort', |
| 242 | + e => results.push('inner signal abort')); |
| 243 | + |
| 244 | + // No values or completion. We'll just wait for the subscriber to abort |
| 245 | + // its subscription. |
| 246 | + }); |
| 247 | + }); |
| 248 | + |
| 249 | + const ac = new AbortController(); |
| 250 | + ac.signal.addEventListener('abort', e => results.push('outer signal abort')); |
| 251 | + catchObservable.subscribe({}, {signal: ac.signal}); |
| 252 | + ac.abort(); |
| 253 | + |
| 254 | + assert_array_equals(results, ['inner signal abort', 'inner teardown', 'outer signal abort']); |
| 255 | +}, "catch(): Abort order between outer AbortSignal and inner CatchHandler subscriber's AbortSignal"); |
0 commit comments