import 'dart:async'; import 'package:angel3_reactivex/angel3_reactivex.dart'; import 'package:test/test.dart'; import '../utils.dart'; Stream _getStream() { final controller = StreamController(); Timer(const Duration(milliseconds: 10), () => controller.add(1)); Timer(const Duration(milliseconds: 20), () => controller.add(2)); Timer(const Duration(milliseconds: 30), () => controller.add(3)); Timer(const Duration(milliseconds: 40), () { controller.add(4); controller.close(); }); return controller.stream; } Stream _getOtherStream(int value) { final controller = StreamController(); Timer(const Duration(milliseconds: 15), () => controller.add(value + 1)); Timer(const Duration(milliseconds: 25), () => controller.add(value + 2)); Timer(const Duration(milliseconds: 35), () => controller.add(value + 3)); Timer(const Duration(milliseconds: 45), () { controller.add(value + 4); controller.close(); }); return controller.stream; } Stream range() => Stream.fromIterable(const [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); void main() { test('Rx.switchMap', () async { const expectedOutput = [5, 6, 7, 8]; var count = 0; _getStream().switchMap(_getOtherStream).listen(expectAsync1((result) { expect(result, expectedOutput[count++]); }, count: expectedOutput.length)); }); test('Rx.switchMap.reusable', () async { final transformer = SwitchMapStreamTransformer(_getOtherStream); const expectedOutput = [5, 6, 7, 8]; var countA = 0, countB = 0; _getStream().transform(transformer).listen(expectAsync1((result) { expect(result, expectedOutput[countA++]); }, count: expectedOutput.length)); _getStream().transform(transformer).listen(expectAsync1((result) { expect(result, expectedOutput[countB++]); }, count: expectedOutput.length)); }); test('Rx.switchMap.asBroadcastStream', () async { final stream = _getStream().asBroadcastStream().switchMap(_getOtherStream); // listen twice on same stream stream.listen(null); stream.listen(null); // code should reach here await expectLater(true, true); }); test('Rx.switchMap.error.shouldThrowA', () async { final streamWithError = Stream.error(Exception()).switchMap(_getOtherStream); streamWithError.listen(null, onError: expectAsync2((Exception e, StackTrace s) { expect(e, isException); })); }); test('Rx.switchMap.error.shouldThrowB', () async { final streamWithError = Stream.value(1).switchMap( (_) => Stream.error(Exception('Catch me if you can!'))); streamWithError.listen(null, onError: expectAsync2((Exception e, StackTrace s) { expect(e, isException); })); }); test('Rx.switchMap.error.shouldThrowC', () async { final streamWithError = Stream.value(1).switchMap((_) { throw Exception('oh noes!'); }); streamWithError.listen(null, onError: expectAsync2((Exception e, StackTrace s) { expect(e, isException); })); }); test('Rx.switchMap.pause.resume', () async { late StreamSubscription subscription; final stream = Stream.value(0).switchMap((_) => Stream.value(1)); subscription = stream.listen(expectAsync1((value) { expect(value, 1); subscription.cancel(); }, count: 1)); subscription.pause(); subscription.resume(); }); test('Rx.switchMap stream close after switch', () async { final controller = StreamController(); final list = controller.stream .switchMap((it) => Stream.fromIterable([it, it])) .toList(); controller.add(1); await Future.delayed(Duration(microseconds: 1)); controller.add(2); await controller.close(); expect(await list, [1, 1, 2, 2]); }); test('Rx.switchMap accidental broadcast', () async { final controller = StreamController(); final stream = controller.stream.switchMap((_) => Stream.empty()); stream.listen(null); expect(() => stream.listen(null), throwsStateError); controller.add(1); }); test('Rx.switchMap closes after the last inner Stream closed - issue/511', () async { final outer = StreamController(); final inner = BehaviorSubject.seeded(false); final stream = outer.stream.switchMap((_) => inner.stream); expect(stream, emitsThrough(emitsDone)); outer.add(true); await Future.delayed(Duration.zero); await inner.close(); await outer.close(); }); test('Rx.switchMap every subscription triggers a listen on the root Stream', () async { var count = 0; final controller = StreamController.broadcast(); final root = OnSubscriptionTriggerableStream(controller.stream, () => count++); final stream = root.switchMap((event) => Stream.value(event)); stream.listen((event) {}); stream.listen((event) {}); expect(count, 2); await controller.close(); }); test('Rx.switchMap.nullable', () { nullableTest( (s) => s.switchMap((v) => Stream.value(v)), ); }); test( 'Rx.switchMap pauses subscription when cancelling inner subscription, then resume', () async { var isController1Cancelled = false; final cancelCompleter1 = Completer.sync(); final controller1 = StreamController() ..add(0) ..add(1) ..onCancel = () async { await Future.delayed(const Duration(milliseconds: 10)); await cancelCompleter1.future; isController1Cancelled = true; }; final controller2 = StreamController() ..add(2) ..add(3) ..onListen = () { expect( isController1Cancelled, true, reason: 'controller1 should be cancelled before controller2 is listened to', ); }; final controller = StreamController>() ..add(controller1); final stream = controller.stream.switchMap((c) => c.stream); var expected = 0; stream.listen( expectAsync1( (v) async { expect(v, expected++); if (v == 1) { // switch to controller2.stream controller.add(controller2); await Future.delayed(const Duration(milliseconds: 10)); cancelCompleter1.complete(null); } }, count: 4, ), ); }, ); test('Rx.switchMap forwards errors from the cancel()', () { var isController1Cancelled = false; final controller1 = StreamController() ..add(0) ..add(1) ..onCancel = () async { await Future.delayed(const Duration(milliseconds: 10)); isController1Cancelled = true; throw Exception('cancel error'); }; final controller2 = StreamController() ..add(2) ..add(3) ..onListen = () { expect( isController1Cancelled, true, reason: 'controller1 should be cancelled before controller2 is listened to', ); }; final controller = StreamController>() ..add(controller1); final stream = controller.stream.switchMap((c) => c.stream); var expected = 0; stream.listen( expectAsync1( (v) async { expect(v, expected++); if (v == 1) { // switch to controller2.stream controller.add(controller2); } }, count: 4, ), onError: expectAsync1( (Object error) => expect(error, isException), count: 1, ), ); }); test( 'Rx.switchMap pauses the next inner StreamSubscription when pausing while cancelling the previous inner Stream', () { var isController1Cancelled = false; final cancelCompleter1 = Completer.sync(); final controller1 = StreamController() ..add(0) ..add(1) ..onCancel = () async { await Future.delayed(const Duration(milliseconds: 10)); await cancelCompleter1.future; isController1Cancelled = true; }; final controller2 = StreamController() ..add(2) ..add(3) ..onListen = () { expect( isController1Cancelled, true, reason: 'controller1 should be cancelled before controller2 is listened to', ); }; final controller = StreamController>() ..add(controller1); final stream = controller.stream.switchMap((c) => c.stream); var expected = 0; late StreamSubscription subscription; subscription = stream.listen( expectAsync1( (v) async { expect(v, expected++); if (v == 1) { // switch to controller2.stream controller.add(controller2); await Future.delayed(const Duration(milliseconds: 10)); // pauses the subscription while cancelling the controller1 subscription.pause(); // let the cancellation of controller1 complete cancelCompleter1.complete(null); // make sure the controller2.stream is added to the controller await pumpEventQueue(); // controller2.stream should be paused expect(controller2.isPaused, true); // resume the subscription to continue the rest of the stream subscription.resume(); } }, count: 4, ), ); }, ); } class OnSubscriptionTriggerableStream extends Stream { final Stream inner; final void Function() onSubscribe; OnSubscriptionTriggerableStream(this.inner, this.onSubscribe); @override bool get isBroadcast => inner.isBroadcast; @override StreamSubscription listen(void Function(T event)? onData, {Function? onError, void Function()? onDone, bool? cancelOnError}) { onSubscribe(); return inner.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } }