Repository: felangel/bloc (dart)

Issue:
feat: BlocObserver with onDone callback
Add [callback inside BLoC](https://github.com/felangel/bloc/blob/master/packages/bloc/lib/src/bloc.dart#L216) for BlocObserver:
```dart
void onDone(Bloc bloc, Object? event, Object? error, StackTrace? stackTrace)
```
or rename `onEvent` to `onEventStarted` and add `onEventFinished`
`onDone` event must be always called if `onEvent` called, no matter if an error occurred or not.

This is the only proposal for the main idea of the wanted API.

Why do I want this feature?
Because is very useful to know, how long events are executed and successful or not.

I want integrate it with analytics and [Sentry](https://pub.dev/packages/sentry_flutter) transactions:
```dart
final tx = Sentry.startTransaction(bloc, event);
// ...
tx.finish(status: SpanStatus.ok());
```

And get reports something like this:
![image](https://user-images.githubusercontent.com/7805176/149956646-d5861f6d-9319-46c4-8170-0cb81ec75a0b.png)

Because BLoC's are always root for every business logic - this is very handy data.
We can know, how often users doing some events/features, how long they are processing.
Rate successful/error. What's error occurred inside BLoCs and events.

Pull request description:
<!--
  Thanks for contributing!

  Provide a description of your changes below and a general summary in the title

  Please look at the following checklist to ensure that your PR can be accepted quickly:
-->

## Status

**READY**

## Breaking Changes

NO

## Description

<!--- Describe your changes in detail -->
- feat(bloc): add `onDone` callback (closes #3148)

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [X] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore

Patch:
diff --git a/packages/bloc/lib/src/bloc.dart b/packages/bloc/lib/src/bloc.dart
index 252f2227a0c..f72b30d2ad6 100644
--- a/packages/bloc/lib/src/bloc.dart
+++ b/packages/bloc/lib/src/bloc.dart
@@ -215,7 +215,7 @@ abstract class Bloc<Event, State> extends BlocBase<State>
         );
 
         Future<void> handleEvent() async {
-          void onDone() {
+          void tearDown() {
             emitter.complete();
             _emitters.remove(emitter);
             if (!controller.isClosed) controller.close();
@@ -224,11 +224,13 @@ abstract class Bloc<Event, State> extends BlocBase<State>
           try {
             _emitters.add(emitter);
             await handler(event as E, emitter);
+            onDone(event);
           } catch (error, stackTrace) {
             onError(error, stackTrace);
+            onDone(event as E, error, stackTrace);
             rethrow;
           } finally {
-            onDone();
+            tearDown();
           }
         }
 
@@ -268,6 +270,35 @@ abstract class Bloc<Event, State> extends BlocBase<State>
     _blocObserver.onTransition(this, transition);
   }
 
+  /// Called whenever an [event] handler for a specific [Bloc] has completed.
+  /// This may include an [error] and [stackTrace] if an uncaught exception
+  /// occurred within the event handler.
+  ///
+  /// [onDone] is called right after the event handler has completed.
+  /// A great spot to add logging/analytics at the individual [Bloc] level.
+  ///
+  /// **Note: `super.onDone` should always be called first.**
+  /// ```dart
+  /// @override
+  /// void onDone(Event event, [Object? error, StackTrace? stackTrace]) {
+  ///   // Always call super.onDone with the respective event.
+  ///   super.onDone(event, error, stackTrace);
+  ///
+  ///   // Custom onDone logic goes here
+  /// }
+  /// ```
+  ///
+  /// See also:
+  ///
+  /// * [BlocObserver.onDone] for observing event handler completions globally.
+  ///
+  @protected
+  @mustCallSuper
+  void onDone(Event event, [Object? error, StackTrace? stackTrace]) {
+    // ignore: invalid_use_of_protected_member
+    _blocObserver.onDone(this, event, error, stackTrace);
+  }
+
   /// Closes the `event` and `state` `Streams`.
   /// This method should be called when a [Bloc] is no longer needed.
   /// Once [close] is called, `events` that are [add]ed will not be
diff --git a/packages/bloc/lib/src/bloc_observer.dart b/packages/bloc/lib/src/bloc_observer.dart
index 3877eeae98f..adad8ea6fa3 100644
--- a/packages/bloc/lib/src/bloc_observer.dart
+++ b/packages/bloc/lib/src/bloc_observer.dart
@@ -48,6 +48,18 @@ abstract class BlocObserver {
   @mustCallSuper
   void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {}
 
+  /// Called whenever an [event] handler for a specific [bloc] has completed.
+  /// This may include an [error] and [stackTrace] if an uncaught exception
+  /// occurred within the event handler.
+  @protected
+  @mustCallSuper
+  void onDone(
+    Bloc<dynamic, dynamic> bloc,
+    Object? event, [
+    Object? error,
+    StackTrace? stackTrace,
+  ]) {}
+
   /// Called whenever a [Bloc] is closed.
   /// [onClose] is called just before the [Bloc] is closed
   /// and indicates that the particular instance will no longer


Test patch:
diff --git a/packages/bloc/test/bloc_observer_test.dart b/packages/bloc/test/bloc_observer_test.dart
index 69329246aca..5a2f929c93c 100644
--- a/packages/bloc/test/bloc_observer_test.dart
+++ b/packages/bloc/test/bloc_observer_test.dart
@@ -1,3 +1,4 @@
+// ignore_for_file: invalid_use_of_protected_member
 import 'package:bloc/bloc.dart';
 import 'package:test/test.dart';
 
@@ -21,42 +22,42 @@ void main() {
   group('BlocObserver', () {
     group('onCreate', () {
       test('does nothing by default', () {
-        // ignore: invalid_use_of_protected_member
         const DefaultBlocObserver().onCreate(bloc);
       });
     });
 
     group('onEvent', () {
       test('does nothing by default', () {
-        // ignore: invalid_use_of_protected_member
         const DefaultBlocObserver().onEvent(bloc, event);
       });
     });
 
     group('onChange', () {
       test('does nothing by default', () {
-        // ignore: invalid_use_of_protected_member
         const DefaultBlocObserver().onChange(bloc, change);
       });
     });
 
     group('onTransition', () {
       test('does nothing by default', () {
-        // ignore: invalid_use_of_protected_member
         const DefaultBlocObserver().onTransition(bloc, transition);
       });
     });
 
+    group('onDone', () {
+      test('does nothing by default', () {
+        const DefaultBlocObserver().onDone(bloc, event);
+      });
+    });
+
     group('onError', () {
       test('does nothing by default', () {
-        // ignore: invalid_use_of_protected_member
         const DefaultBlocObserver().onError(bloc, error, stackTrace);
       });
     });
 
     group('onClose', () {
       test('does nothing by default', () {
-        // ignore: invalid_use_of_protected_member
         const DefaultBlocObserver().onClose(bloc);
       });
     });
diff --git a/packages/bloc/test/bloc_test.dart b/packages/bloc/test/bloc_test.dart
index e6205412bf7..a9d88dd2fc6 100644
--- a/packages/bloc/test/bloc_test.dart
+++ b/packages/bloc/test/bloc_test.dart
@@ -1,3 +1,4 @@
+// ignore_for_file: invalid_use_of_protected_member
 import 'dart:async';
 
 import 'package:bloc/bloc.dart';
@@ -26,14 +27,12 @@ void main() {
 
       test('triggers onCreate on observer when instantiated', () {
         final bloc = SimpleBloc();
-        // ignore: invalid_use_of_protected_member
         verify(() => observer.onCreate(bloc)).called(1);
       });
 
       test('triggers onClose on observer when closed', () async {
         final bloc = SimpleBloc();
         await bloc.close();
-        // ignore: invalid_use_of_protected_member
         verify(() => observer.onClose(bloc)).called(1);
       });
 
@@ -57,24 +56,25 @@ void main() {
           simpleBloc.stream,
           emitsInOrder(expectedStates),
         ).then((dynamic _) {
-          verify(
-            // ignore: invalid_use_of_protected_member
+          verifyInOrder([
+            () => observer.onCreate(simpleBloc),
+            () => observer.onEvent(simpleBloc, 'event'),
             () => observer.onTransition(
-              simpleBloc,
-              const Transition<dynamic, String>(
-                currentState: '',
-                event: 'event',
-                nextState: 'data',
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
+                  simpleBloc,
+                  const Transition<dynamic, String>(
+                    currentState: '',
+                    event: 'event',
+                    nextState: 'data',
+                  ),
+                ),
             () => observer.onChange(
-              simpleBloc,
-              const Change<String>(currentState: '', nextState: 'data'),
-            ),
-          ).called(1);
+                  simpleBloc,
+                  const Change<String>(currentState: '', nextState: 'data'),
+                ),
+            () => observer.onDone(simpleBloc, 'event'),
+            () => observer.onClose(simpleBloc),
+          ]);
+
           expect(simpleBloc.state, 'data');
         });
 
@@ -91,24 +91,27 @@ void main() {
           simpleBloc.stream,
           emitsInOrder(expectedStates),
         ).then((dynamic _) {
-          verify(
-            // ignore: invalid_use_of_protected_member
+          verifyInOrder([
+            () => observer.onCreate(simpleBloc),
+            () => observer.onEvent(simpleBloc, 'event1'),
+            () => observer.onEvent(simpleBloc, 'event2'),
+            () => observer.onEvent(simpleBloc, 'event3'),
             () => observer.onTransition(
-              simpleBloc,
-              const Transition<dynamic, String>(
-                currentState: '',
-                event: 'event1',
-                nextState: 'data',
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
+                  simpleBloc,
+                  const Transition<dynamic, String>(
+                    currentState: '',
+                    event: 'event1',
+                    nextState: 'data',
+                  ),
+                ),
             () => observer.onChange(
-              simpleBloc,
-              const Change<String>(currentState: '', nextState: 'data'),
-            ),
-          ).called(1);
+                  simpleBloc,
+                  const Change<String>(currentState: '', nextState: 'data'),
+                ),
+            () => observer.onDone(simpleBloc, 'event1'),
+            () => observer.onClose(simpleBloc),
+          ]);
+
           expect(simpleBloc.state, 'data');
         });
 
@@ -174,31 +177,34 @@ void main() {
           complexBloc.stream,
           emitsInOrder(expectedStates),
         ).then((dynamic _) {
-          verify(
-            // ignore: invalid_use_of_protected_member
+          verifyInOrder([
+            () => observer.onCreate(complexBloc),
+            () => observer.onEvent(complexBloc, ComplexEventB()),
             () => observer.onTransition(
-              complexBloc,
-              Transition<ComplexEvent, ComplexState>(
-                currentState: ComplexStateA(),
-                event: ComplexEventB(),
-                nextState: ComplexStateB(),
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
+                  complexBloc,
+                  Transition<ComplexEvent, ComplexState>(
+                    currentState: ComplexStateA(),
+                    event: ComplexEventB(),
+                    nextState: ComplexStateB(),
+                  ),
+                ),
             () => observer.onChange(
-              complexBloc,
-              Change<ComplexState>(
-                currentState: ComplexStateA(),
-                nextState: ComplexStateB(),
-              ),
-            ),
-          ).called(1);
+                  complexBloc,
+                  Change<ComplexState>(
+                    currentState: ComplexStateA(),
+                    nextState: ComplexStateB(),
+                  ),
+                ),
+            () => observer.onDone(complexBloc, ComplexEventB()),
+            () => observer.onClose(complexBloc),
+          ]);
+
           expect(complexBloc.state, ComplexStateB());
         });
 
-        complexBloc.add(ComplexEventB());
+        complexBloc
+          ..add(ComplexEventB())
+          ..close();
       });
 
       test('should map multiple events to correct states', () async {
@@ -291,24 +297,26 @@ void main() {
           emitsInOrder(expectedStates),
         ).then((dynamic _) {
           expectLater(transitions, expectedTransitions);
-          verify(
-            // ignore: invalid_use_of_protected_member
+
+          verifyInOrder([
+            () => observer.onCreate(counterBloc),
+            () => observer.onEvent(counterBloc, CounterEvent.increment),
             () => observer.onTransition(
-              counterBloc,
-              const Transition<CounterEvent, int>(
-                currentState: 0,
-                event: CounterEvent.increment,
-                nextState: 1,
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
+                  counterBloc,
+                  const Transition<CounterEvent, int>(
+                    currentState: 0,
+                    event: CounterEvent.increment,
+                    nextState: 1,
+                  ),
+                ),
             () => observer.onChange(
-              counterBloc,
-              const Change<int>(currentState: 0, nextState: 1),
-            ),
-          ).called(1);
+                  counterBloc,
+                  const Change<int>(currentState: 0, nextState: 1),
+                ),
+            () => observer.onDone(counterBloc, CounterEvent.increment),
+            () => observer.onClose(counterBloc),
+          ]);
+
           expect(counterBloc.state, 1);
         });
 
@@ -336,60 +344,54 @@ void main() {
           emitsInOrder(expectedStates),
         ).then((dynamic _) {
           expect(transitions, expectedTransitions);
-          verify(
-            // ignore: invalid_use_of_protected_member
+
+          verifyInOrder([
+            () => observer.onCreate(counterBloc),
+            () => observer.onEvent(counterBloc, CounterEvent.increment),
+            () => observer.onEvent(counterBloc, CounterEvent.increment),
+            () => observer.onEvent(counterBloc, CounterEvent.increment),
             () => observer.onTransition(
-              counterBloc,
-              const Transition<CounterEvent, int>(
-                currentState: 0,
-                event: CounterEvent.increment,
-                nextState: 1,
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
+                  counterBloc,
+                  const Transition<CounterEvent, int>(
+                    currentState: 0,
+                    event: CounterEvent.increment,
+                    nextState: 1,
+                  ),
+                ),
             () => observer.onChange(
-              counterBloc,
-              const Change<int>(currentState: 0, nextState: 1),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
+                  counterBloc,
+                  const Change<int>(currentState: 0, nextState: 1),
+                ),
+            () => observer.onDone(counterBloc, CounterEvent.increment),
             () => observer.onTransition(
-              counterBloc,
-              const Transition<CounterEvent, int>(
-                currentState: 1,
-                event: CounterEvent.increment,
-                nextState: 2,
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
+                  counterBloc,
+                  const Transition<CounterEvent, int>(
+                    currentState: 1,
+                    event: CounterEvent.increment,
+                    nextState: 2,
+                  ),
+                ),
             () => observer.onChange(
-              counterBloc,
-              const Change<int>(currentState: 1, nextState: 2),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
+                  counterBloc,
+                  const Change<int>(currentState: 1, nextState: 2),
+                ),
             () => observer.onTransition(
-              counterBloc,
-              const Transition<CounterEvent, int>(
-                currentState: 2,
-                event: CounterEvent.increment,
-                nextState: 3,
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
+                  counterBloc,
+                  const Transition<CounterEvent, int>(
+                    currentState: 2,
+                    event: CounterEvent.increment,
+                    nextState: 3,
+                  ),
+                ),
             () => observer.onChange(
-              counterBloc,
-              const Change<int>(currentState: 2, nextState: 3),
-            ),
-          ).called(1);
+                  counterBloc,
+                  const Change<int>(currentState: 2, nextState: 3),
+                ),
+            () => observer.onDone(counterBloc, CounterEvent.increment),
+            () => observer.onClose(counterBloc),
+          ]);
+
+          expect(counterBloc.state, equals(3));
         });
 
         counterBloc
@@ -513,7 +515,8 @@ void main() {
         await asyncBloc.close();
 
         expect(states, expectedStates);
-        // ignore: invalid_use_of_protected_member
+
+        verify(() => observer.onDone(asyncBloc, AsyncEvent())).called(1);
         verifyNever(() => observer.onError(any(), any(), any()));
       });
 
@@ -533,80 +536,75 @@ void main() {
           asyncBloc.stream,
           emitsInOrder(expectedStates),
         ).then((dynamic _) {
-          verify(
-            // ignore: invalid_use_of_protected_member
+          verifyInOrder([
+            () => observer.onCreate(asyncBloc),
+            () => observer.onEvent(asyncBloc, AsyncEvent()),
             () => observer.onTransition(
-              asyncBloc,
-              Transition<AsyncEvent, AsyncState>(
-                currentState: const AsyncState(
-                  isLoading: false,
-                  hasError: false,
-                  isSuccess: false,
+                  asyncBloc,
+                  Transition<AsyncEvent, AsyncState>(
+                    currentState: const AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                    event: AsyncEvent(),
+                    nextState: const AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                  ),
                 ),
-                event: AsyncEvent(),
-                nextState: const AsyncState(
-                  isLoading: true,
-                  hasError: false,
-                  isSuccess: false,
-                ),
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
             () => observer.onChange(
-              asyncBloc,
-              const Change<AsyncState>(
-                currentState: AsyncState(
-                  isLoading: false,
-                  hasError: false,
-                  isSuccess: false,
+                  asyncBloc,
+                  const Change<AsyncState>(
+                    currentState: AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                    nextState: AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                  ),
                 ),
-                nextState: AsyncState(
-                  isLoading: true,
-                  hasError: false,
-                  isSuccess: false,
-                ),
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
             () => observer.onTransition(
-              asyncBloc,
-              Transition<AsyncEvent, AsyncState>(
-                currentState: const AsyncState(
-                  isLoading: true,
-                  hasError: false,
-                  isSuccess: false,
+                  asyncBloc,
+                  Transition<AsyncEvent, AsyncState>(
+                    currentState: const AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                    event: AsyncEvent(),
+                    nextState: const AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: true,
+                    ),
+                  ),
                 ),
-                event: AsyncEvent(),
-                nextState: const AsyncState(
-                  isLoading: false,
-                  hasError: false,
-                  isSuccess: true,
-                ),
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
             () => observer.onChange(
-              asyncBloc,
-              const Change<AsyncState>(
-                currentState: AsyncState(
-                  isLoading: true,
-                  hasError: false,
-                  isSuccess: false,
-                ),
-                nextState: AsyncState(
-                  isLoading: false,
-                  hasError: false,
-                  isSuccess: true,
+                  asyncBloc,
+                  const Change<AsyncState>(
+                    currentState: AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                    nextState: AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: true,
+                    ),
+                  ),
                 ),
-              ),
-            ),
-          ).called(1);
+            () => observer.onDone(asyncBloc, AsyncEvent()),
+            () => observer.onClose(asyncBloc),
+          ]);
+
           expect(
             asyncBloc.state,
             const AsyncState(
@@ -636,80 +634,139 @@ void main() {
           asyncBloc.stream,
           emitsInOrder(expectedStates),
         ).then((dynamic _) {
-          verify(
-            // ignore: invalid_use_of_protected_member
+          verifyInOrder([
+            () => observer.onCreate(asyncBloc),
+            () => observer.onEvent(asyncBloc, AsyncEvent()),
+            () => observer.onEvent(asyncBloc, AsyncEvent()),
             () => observer.onTransition(
-              asyncBloc,
-              Transition<AsyncEvent, AsyncState>(
-                currentState: const AsyncState(
-                  isLoading: false,
-                  hasError: false,
-                  isSuccess: false,
+                  asyncBloc,
+                  Transition<AsyncEvent, AsyncState>(
+                    currentState: const AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                    event: AsyncEvent(),
+                    nextState: const AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                  ),
                 ),
-                event: AsyncEvent(),
-                nextState: const AsyncState(
-                  isLoading: true,
-                  hasError: false,
-                  isSuccess: false,
-                ),
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
             () => observer.onChange(
-              asyncBloc,
-              const Change<AsyncState>(
-                currentState: AsyncState(
-                  isLoading: false,
-                  hasError: false,
-                  isSuccess: false,
+                  asyncBloc,
+                  const Change<AsyncState>(
+                    currentState: AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                    nextState: AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                  ),
                 ),
-                nextState: AsyncState(
-                  isLoading: true,
-                  hasError: false,
-                  isSuccess: false,
-                ),
-              ),
-            ),
-          ).called(1);
-          verify(
-            // ignore: invalid_use_of_protected_member
             () => observer.onTransition(
-              asyncBloc,
-              Transition<AsyncEvent, AsyncState>(
-                currentState: const AsyncState(
-                  isLoading: true,
-                  hasError: false,
-                  isSuccess: false,
+                  asyncBloc,
+                  Transition<AsyncEvent, AsyncState>(
+                    currentState: const AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                    event: AsyncEvent(),
+                    nextState: const AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: true,
+                    ),
+                  ),
                 ),
-                event: AsyncEvent(),
-                nextState: const AsyncState(
-                  isLoading: false,
-                  hasError: false,
-                  isSuccess: true,
+            () => observer.onChange(
+                  asyncBloc,
+                  const Change<AsyncState>(
+                    currentState: AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                    nextState: AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: true,
+                    ),
+                  ),
+                ),
+            () => observer.onDone(asyncBloc, AsyncEvent()),
+            () => observer.onTransition(
+                  asyncBloc,
+                  Transition<AsyncEvent, AsyncState>(
+                    currentState: const AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: true,
+                    ),
+                    event: AsyncEvent(),
+                    nextState: const AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                  ),
                 ),
-              ),
-            ),
-          ).called(2);
-          verify(
-            // ignore: invalid_use_of_protected_member
             () => observer.onChange(
-              asyncBloc,
-              const Change<AsyncState>(
-                currentState: AsyncState(
-                  isLoading: true,
-                  hasError: false,
-                  isSuccess: false,
+                  asyncBloc,
+                  const Change<AsyncState>(
+                    currentState: AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: true,
+                    ),
+                    nextState: AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                  ),
                 ),
-                nextState: AsyncState(
-                  isLoading: false,
-                  hasError: false,
-                  isSuccess: true,
+            () => observer.onTransition(
+                  asyncBloc,
+                  Transition<AsyncEvent, AsyncState>(
+                    currentState: const AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                    event: AsyncEvent(),
+                    nextState: const AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: true,
+                    ),
+                  ),
                 ),
-              ),
-            ),
-          ).called(2);
+            () => observer.onChange(
+                  asyncBloc,
+                  const Change<AsyncState>(
+                    currentState: AsyncState(
+                      isLoading: true,
+                      hasError: false,
+                      isSuccess: false,
+                    ),
+                    nextState: AsyncState(
+                      isLoading: false,
+                      hasError: false,
+                      isSuccess: true,
+                    ),
+                  ),
+                ),
+            () => observer.onDone(asyncBloc, AsyncEvent()),
+            () => observer.onClose(asyncBloc),
+          ]);
+
           expect(
             asyncBloc.state,
             const AsyncState(
@@ -1351,7 +1408,6 @@ void main() {
           OnExceptionBloc(
             exception: expectedError,
             onErrorCallback: (Object _, StackTrace __) {},
-            // ignore: invalid_use_of_protected_member
           ).addError(expectedError, StackTrace.current);
         }, (Object error, StackTrace stackTrace) {
           expect(error, equals(expectedError));
@@ -1465,7 +1521,7 @@ void main() {
         );
       });
 
-      test('triggers onError from mapEventToState', () {
+      test('triggers onError from event handler', () {
         runZonedGuarded(
           () {
             final error = Error();