diff --git a/.github/workflows/hydrated_bloc.yaml b/.github/workflows/hydrated_bloc.yaml index 66b69f6ade1..7055b38a6d7 100644 --- a/.github/workflows/hydrated_bloc.yaml +++ b/.github/workflows/hydrated_bloc.yaml @@ -33,7 +33,7 @@ jobs: run: flutter analyze lib test example - name: Run tests - run: flutter test -j 1 --no-pub --coverage --test-randomize-ordering-seed random + run: flutter test --no-pub --coverage --test-randomize-ordering-seed random - name: Check Code Coverage uses: VeryGoodOpenSource/very_good_coverage@v1.2.0 diff --git a/README.md b/README.md index 8883d4d25bd..18a6bfe70a1 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,10 @@ Our top sponsors are shown below! [[Become a Sponsor](https://github.com/sponsor
Try the Flutter Chat Tutorial  💬 - + + + + diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 0c47a490f6c..54dd9a14489 100644 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -22,17 +22,16 @@ Get Started

-

Sponsored with 💖 by

+

Sponsored with 💖 by

- - - - - - - -
- - -
Try the Flutter Chat Tutorial  💬 -
+
+
+ +
+
+
Try the Flutter Chat Tutorial  💬 +
+
+ +
+
diff --git a/docs/_snippets/core_concepts/simple_bloc_observer_on_change_usage.dart.md b/docs/_snippets/core_concepts/simple_bloc_observer_on_change_usage.dart.md index 173a413d091..3d70eb54eff 100644 --- a/docs/_snippets/core_concepts/simple_bloc_observer_on_change_usage.dart.md +++ b/docs/_snippets/core_concepts/simple_bloc_observer_on_change_usage.dart.md @@ -1,8 +1,12 @@ ```dart void main() { - Bloc.observer = SimpleBlocObserver(); - CounterCubit() - ..increment() - ..close(); + BlocOverrides.runZoned( + () { + CounterCubit() + ..increment() + ..close(); + }, + blocObserver: SimpleBlocObserver(), + ); } -``` \ No newline at end of file +``` diff --git a/docs/_snippets/core_concepts/simple_bloc_observer_on_transition_usage.dart.md b/docs/_snippets/core_concepts/simple_bloc_observer_on_transition_usage.dart.md index 74e8cd604a6..bd2db81040a 100644 --- a/docs/_snippets/core_concepts/simple_bloc_observer_on_transition_usage.dart.md +++ b/docs/_snippets/core_concepts/simple_bloc_observer_on_transition_usage.dart.md @@ -1,8 +1,12 @@ ```dart void main() { - Bloc.observer = SimpleBlocObserver(); - CounterBloc() - ..add(Increment()) - ..close(); + BlocOverrides.runZoned( + () { + CounterBloc() + ..add(Increment()) + ..close(); + }, + blocObserver: SimpleBlocObserver(), + ); } -``` \ No newline at end of file +``` diff --git a/docs/assets/style.css b/docs/assets/style.css index 433bb61f586..bf63272d92d 100644 --- a/docs/assets/style.css +++ b/docs/assets/style.css @@ -54,3 +54,15 @@ section.cover blockquote > .buttons > a:hover { #TOPBANNER p { padding: 1.25rem 0; } + +#cover-sponsors-grid { + display: grid; + grid-auto-rows: 1fr; + grid-template-columns: 1fr 1fr 1fr; + gap: 0px 1em; + max-width: 600px; + justify-content: center; + align-items: center; + margin: 0 auto; + font-size: 0.8em; +} diff --git a/docs/migration.md b/docs/migration.md index d64a614cf0e..6df6a2d53ad 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,6 +2,243 @@ ?> 💡 **Tip**: Please refer to the [release log](https://github.com/felangel/bloc/releases) for more information regarding what changed in each release. +## v8.0.0 + +### package:bloc + +#### ❗✨ Introduce new `BlocOverrides` API + +!> In bloc v8.0.0, `Bloc.observer` and `Bloc.transformer` were removed in favor of the `BlocOverrides` API. + +##### Rationale + +The previous API used to override the default `BlocObserver` and `EventTransformer` relied on a global singleton for both the `BlocObserver` and `EventTransformer`. + +As a result, it was not possible to: + +- Have multiple `BlocObserver` or `EventTransformer` implementations scoped to different parts of the application +- Have `BlocObserver` or `EventTransformer` overrides be scoped to a package + - If a package were to depend on `package:bloc` and registered its own `BlocObserver`, any consumer of the package would either have to overwrite the package's `BlocObserver` or report to the package's `BlocObserver`. + +It was also more difficult to test because of the shared global state across tests. + +Bloc v8.0.0 introduces a `BlocOverrides` class which allows developers to override `BlocObserver` and/or `EventTransformer` for a specific `Zone` rather than relying on a global mutable singleton. + +**v7.x.x** + +```dart +void main() { + Bloc.observer = CustomBlocObserver(); + Bloc.transformer = customEventTransformer(); + + // ... +} +``` + +**v8.0.0** + +```dart +void main() { + BlocOverrides.runZoned( + () { + // ... + }, + blocObserver: CustomBlocObserver(), + eventTransformer: customEventTransformer(), + ); +} +``` + +`Bloc` instances will use the `BlocObserver` and/or `EventTransformer` for the current `Zone` via `BlocOverrides.current`. If there are no `BlocOverrides` for the zone, they will use the existing internal defaults (no change in behavior/functionality). + +This allows allow each `Zone` to function independently with its own `BlocOverrides`. + +```dart +BlocOverrides.runZoned( + () { + // BlocObserverA and eventTransformerA + final overrides = BlocOverrides.current; + + // Blocs in this zone report to BlocObserverA + // and use eventTransformerA as the default transformer. + // ... + + // Later... + BlocOverrides.runZoned( + () { + // BlocObserverB and eventTransformerB + final overrides = BlocOverrides.current; + + // Blocs in this zone report to BlocObserverB + // and use eventTransformerB as the default transformer. + // ... + }, + blocObserver: BlocObserverB(), + eventTransformer: eventTransformerB(), + ); + }, + blocObserver: BlocObserverA(), + eventTransformer: eventTransformerA(), +); +``` + +#### ❗✨ Improve Error Handling and Reporting + +!> In bloc v8.0.0, `BlocUnhandledErrorException` is removed. In addition, any uncaught exceptions are always reported to `onError` and rethrown (regardless of debug or release mode). The `addError` API reports errors to `onError`, but does not treat reported errors as uncaught exceptions. + +##### Rationale + +The goal of these changes is: + +- make internal unhandled exceptions extremely obvious while still preserving bloc functionality +- support `addError` without disrupting control flow + +Previously, error handling and reporting varied depending on whether the application was running in debug or release mode. In addition, errors reported via `addError` were treated as uncaught exceptions in debug mode which led to a poor developer experience when using the `addError` API (specifically when writing unit tests). + +In v8.0.0, `addError` can be safely used to report errors and `blocTest` can be used to verify that errors are reported. All errors are still reported to `onError`, however, only uncaught exceptions are rethrown (regardless of debug or release mode). + +#### ❗🧹 Make `BlocObserver` abstract + +!> In bloc v8.0.0, `BlocObserver` was converted into an `abstract` class which means an instance of `BlocObserver` cannot be instantiated. + +##### Rationale + +`BlocObserver` was intended to be an interface. Since the default API implementation are no-ops, `BlocObserver` is now an `abstract` class to clearly communicate that the class is meant to be extended and not directly instantiated. + +**v7.x.x** + +```dart +void main() { + // It was possible to create an instance of the base class. + final observer = BlocObserver(); +} +``` + +**v8.0.0** + +```dart +class MyBlocObserver extends BlocObserver {...} + +void main() { + // Cannot instantiate the base class. + final observer = BlocObserver(); // ERROR + + // Extend `BlocObserver` instead. + final observer = MyBlocObserver(); // OK +} +``` + +#### ❗✨ `add` throws `StateError` if Bloc is closed + +!> In bloc v8.0.0, calling `add` on a closed bloc will result in a `StateError`. + +##### Rationale + +Previously, it was possible to call `add` on a closed bloc and the internal error would get swallowed, making it difficult to debug why the added event was not being processed. In order to make this scenario more visible, in v8.0.0, calling `add` on a closed bloc will throw a `StateError` which will be reported as an uncaught exception and propagated to `onError`. + +#### ❗✨ `emit` throws `StateError` if Bloc is closed + +!> In bloc v8.0.0, calling `emit` within a closed bloc will result in a `StateError`. + +##### Rationale + +Previously, it was possible to call `emit` within a closed bloc and no state change would occur but there would also be no indication of what went wrong, making it difficult to debug. In order to make this scenario more visible, in v8.0.0, calling `emit` within a closed bloc will throw a `StateError` which will be reported as an uncaught exception and propagated to `onError`. + +#### ❗🧹 Remove Deprecated APIs + +!> In bloc v8.0.0, all previously deprecated APIs were removed. + +##### Summary + +- `mapEventToState` removed in favor of `on` +- `transformEvents` removed in favor of `EventTransformer` API +- `TransitionFunction` typedef removed in favor of `EventTransformer` API +- `listen` removed in favor of `stream.listen` + +### package:bloc_test + +#### ✨ `MockBloc` and `MockCubit` no longer require `registerFallbackValue` + +!> In bloc_test v9.0.0, developers no longer need to explicitly call `registerFallbackValue` when using `MockBloc` or `MockCubit`. + +##### Summary + +`registerFallbackValue` is only needed when using the `any()` matcher from `package:mocktail` for a custom type. Previously, `registerFallbackValue` was needed for every `Event` and `State` when using `MockBloc` or `MockCubit`. + +**v8.x.x** + +```dart +class FakeMyEvent extends Fake implements MyEvent {} +class FakeMyState extends Fake implements MyState {} +class MyMockBloc extends MockBloc implements MyBloc {} + +void main() { + setUpAll(() { + registerFallbackValue(FakeMyEvent()); + registerFallbackValue(FakeMyState()); + }); + + // Tests... +} +``` + +**v9.0.0** + +```dart +class MyMockBloc extends MockBloc implements MyBloc {} + +void main() { + // Tests... +} +``` + +### package:hydrated_bloc + +#### ❗✨ Introduce new `HydratedBlocOverrides` API + +!> In hydrated_bloc v8.0.0, `HydratedBloc.storage` was removed in favor of the `HydratedBlocOverrides` API. + +##### Rationale + +Previously, a global singleton was used to override the `Storage` implementation. + +As a result, it was not possible to have multiple `Storage` implementations scoped to different parts of the application. It was also more difficult to test because of the shared global state across tests. + +`HydratedBloc` v8.0.0 introduces a `HydratedBlocOverrides` class which allows developers to override `Storage` for a specific `Zone` rather than relying on a global mutable singleton. + +**v7.x.x** + +```dart +void main() async { + HydratedBloc.storage = await HydratedStorage.build( + storageDirectory: await getApplicationSupportDirectory(), + ); + + // ... +} +``` + +**v8.0.0** + +```dart +void main() { + final storage = await HydratedStorage.build( + storageDirectory: await getApplicationSupportDirectory(), + ); + + HydratedBlocOverrides.runZoned( + () { + // ... + }, + storage: storage, + ); +} +``` + +`HydratedBloc` instances will use the `Storage` for the current `Zone` via `HydratedBlocOverrides.current`. + +This allows allow each `Zone` to function independently with its own `BlocOverrides`. + ## v7.2.0 ### package:bloc diff --git a/examples/angular_counter/web/main.dart b/examples/angular_counter/web/main.dart index dd28c5842f3..095e5afe64c 100644 --- a/examples/angular_counter/web/main.dart +++ b/examples/angular_counter/web/main.dart @@ -23,6 +23,8 @@ class SimpleBlocObserver extends BlocObserver { } void main() { - Bloc.observer = SimpleBlocObserver(); - runApp(ng.AppComponentNgFactory); + BlocOverrides.runZoned( + () => runApp(ng.AppComponentNgFactory), + blocObserver: SimpleBlocObserver(), + ); } diff --git a/examples/flutter_bloc_with_stream/test/ticker_page_test.dart b/examples/flutter_bloc_with_stream/test/ticker_page_test.dart index 0c04f63dc81..f8dc72a4250 100644 --- a/examples/flutter_bloc_with_stream/test/ticker_page_test.dart +++ b/examples/flutter_bloc_with_stream/test/ticker_page_test.dart @@ -11,10 +11,6 @@ import 'package:mocktail/mocktail.dart'; class MockTickerBloc extends MockBloc implements TickerBloc {} -class FakeTickerEvent extends Fake implements TickerEvent {} - -class FakeTickerState extends Fake implements TickerState {} - extension on WidgetTester { Future pumpTickerPage(TickerBloc tickerBloc) { return pumpWidget( @@ -28,11 +24,6 @@ extension on WidgetTester { void main() { late TickerBloc tickerBloc; - setUpAll(() { - registerFallbackValue(FakeTickerEvent()); - registerFallbackValue(FakeTickerState()); - }); - setUp(() { tickerBloc = MockTickerBloc(); }); diff --git a/examples/flutter_complex_list/lib/main.dart b/examples/flutter_complex_list/lib/main.dart index 5883358447b..129d20961fc 100644 --- a/examples/flutter_complex_list/lib/main.dart +++ b/examples/flutter_complex_list/lib/main.dart @@ -1,11 +1,13 @@ import 'package:bloc/bloc.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_complex_list/app.dart'; import 'package:flutter_complex_list/repository.dart'; import 'package:flutter_complex_list/simple_bloc_observer.dart'; void main() { - Bloc.observer = SimpleBlocObserver(); - runApp(App(repository: Repository())); + BlocOverrides.runZoned( + () => runApp(App(repository: Repository())), + blocObserver: SimpleBlocObserver(), + ); } diff --git a/examples/flutter_counter/lib/main.dart b/examples/flutter_counter/lib/main.dart index 061f098e0e1..87da92815fc 100644 --- a/examples/flutter_counter/lib/main.dart +++ b/examples/flutter_counter/lib/main.dart @@ -1,10 +1,12 @@ import 'package:bloc/bloc.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'app.dart'; import 'counter_observer.dart'; void main() { - Bloc.observer = CounterObserver(); - runApp(const CounterApp()); + BlocOverrides.runZoned( + () => runApp(const CounterApp()), + blocObserver: CounterObserver(), + ); } diff --git a/examples/flutter_dynamic_form/test/new_car/view/new_car_page_test.dart b/examples/flutter_dynamic_form/test/new_car/view/new_car_page_test.dart index c07ab3da315..75915351416 100644 --- a/examples/flutter_dynamic_form/test/new_car/view/new_car_page_test.dart +++ b/examples/flutter_dynamic_form/test/new_car/view/new_car_page_test.dart @@ -11,10 +11,6 @@ class MockNewCarRepository extends Mock implements NewCarRepository {} class MockNewCarBloc extends MockBloc implements NewCarBloc {} -class FakeNewCarEvent extends Fake implements NewCarEvent {} - -class FakeNewCarState extends Fake implements NewCarState {} - extension on WidgetTester { Future pumpNewCarPage(NewCarRepository newCarRepository) { return pumpWidget( @@ -56,8 +52,6 @@ void main() { final mockYear = mockYears[0]; setUp(() { - registerFallbackValue(FakeNewCarState()); - registerFallbackValue(FakeNewCarEvent()); newCarRepository = MockNewCarRepository(); newCarBloc = MockNewCarBloc(); }); diff --git a/examples/flutter_firebase_login/lib/main.dart b/examples/flutter_firebase_login/lib/main.dart index 1dd1ac35875..809123c0106 100644 --- a/examples/flutter_firebase_login/lib/main.dart +++ b/examples/flutter_firebase_login/lib/main.dart @@ -5,10 +5,12 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_firebase_login/app/app.dart'; Future main() async { - Bloc.observer = AppBlocObserver(); WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); final authenticationRepository = AuthenticationRepository(); await authenticationRepository.user.first; - runApp(App(authenticationRepository: authenticationRepository)); + BlocOverrides.runZoned( + () => runApp(App(authenticationRepository: authenticationRepository)), + blocObserver: AppBlocObserver(), + ); } diff --git a/examples/flutter_firebase_login/test/app/view/app_test.dart b/examples/flutter_firebase_login/test/app/view/app_test.dart index 9d386a9f417..fb0dcf0ed0f 100644 --- a/examples/flutter_firebase_login/test/app/view/app_test.dart +++ b/examples/flutter_firebase_login/test/app/view/app_test.dart @@ -15,20 +15,11 @@ class MockAuthenticationRepository extends Mock class MockAppBloc extends MockBloc implements AppBloc {} -class FakeAppEvent extends Fake implements AppEvent {} - -class FakeAppState extends Fake implements AppState {} - void main() { group('App', () { late AuthenticationRepository authenticationRepository; late User user; - setUpAll(() { - registerFallbackValue(FakeAppEvent()); - registerFallbackValue(FakeAppState()); - }); - setUp(() { authenticationRepository = MockAuthenticationRepository(); user = MockUser(); @@ -54,11 +45,6 @@ void main() { late AuthenticationRepository authenticationRepository; late AppBloc appBloc; - setUpAll(() { - registerFallbackValue(FakeAppEvent()); - registerFallbackValue(FakeAppState()); - }); - setUp(() { authenticationRepository = MockAuthenticationRepository(); appBloc = MockAppBloc(); diff --git a/examples/flutter_firebase_login/test/home/view/home_page_test.dart b/examples/flutter_firebase_login/test/home/view/home_page_test.dart index 808a64bf4e9..d845e56f64e 100644 --- a/examples/flutter_firebase_login/test/home/view/home_page_test.dart +++ b/examples/flutter_firebase_login/test/home/view/home_page_test.dart @@ -9,10 +9,6 @@ import 'package:mocktail/mocktail.dart'; class MockAppBloc extends MockBloc implements AppBloc {} -class FakeAppEvent extends Fake implements AppEvent {} - -class FakeAppState extends Fake implements AppState {} - class MockUser extends Mock implements User {} void main() { @@ -21,11 +17,6 @@ void main() { late AppBloc appBloc; late User user; - setUpAll(() { - registerFallbackValue(FakeAppEvent()); - registerFallbackValue(FakeAppState()); - }); - setUp(() { appBloc = MockAppBloc(); user = MockUser(); diff --git a/examples/flutter_firebase_login/test/login/view/login_form_test.dart b/examples/flutter_firebase_login/test/login/view/login_form_test.dart index 622e128b40f..f4ce84fc0f1 100644 --- a/examples/flutter_firebase_login/test/login/view/login_form_test.dart +++ b/examples/flutter_firebase_login/test/login/view/login_form_test.dart @@ -14,8 +14,6 @@ class MockAuthenticationRepository extends Mock class MockLoginCubit extends MockCubit implements LoginCubit {} -class FakeLoginState extends Fake implements LoginState {} - class MockEmail extends Mock implements Email {} class MockPassword extends Mock implements Password {} @@ -33,10 +31,6 @@ void main() { group('LoginForm', () { late LoginCubit loginCubit; - setUpAll(() { - registerFallbackValue(FakeLoginState()); - }); - setUp(() { loginCubit = MockLoginCubit(); when(() => loginCubit.state).thenReturn(const LoginState()); diff --git a/examples/flutter_firebase_login/test/sign_up/view/sign_up_form_test.dart b/examples/flutter_firebase_login/test/sign_up/view/sign_up_form_test.dart index 86d980548ee..9b7f6f144f6 100644 --- a/examples/flutter_firebase_login/test/sign_up/view/sign_up_form_test.dart +++ b/examples/flutter_firebase_login/test/sign_up/view/sign_up_form_test.dart @@ -13,8 +13,6 @@ class MockAuthenticationRepository extends Mock class MockSignUpCubit extends MockCubit implements SignUpCubit {} -class FakeSignUpState extends Fake implements SignUpState {} - class MockEmail extends Mock implements Email {} class MockPassword extends Mock implements Password {} @@ -35,10 +33,6 @@ void main() { group('SignUpForm', () { late SignUpCubit signUpCubit; - setUpAll(() { - registerFallbackValue(FakeSignUpState()); - }); - setUp(() { signUpCubit = MockSignUpCubit(); when(() => signUpCubit.state).thenReturn(const SignUpState()); diff --git a/examples/flutter_firestore_todos/lib/main.dart b/examples/flutter_firestore_todos/lib/main.dart index edf7e2716c6..f6a739a324e 100644 --- a/examples/flutter_firestore_todos/lib/main.dart +++ b/examples/flutter_firestore_todos/lib/main.dart @@ -9,9 +9,11 @@ import 'package:user_repository/user_repository.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - Bloc.observer = SimpleBlocObserver(); await Firebase.initializeApp(); - runApp(TodosApp()); + BlocOverrides.runZoned( + () => runApp(TodosApp()), + blocObserver: SimpleBlocObserver(), + ); } class TodosApp extends StatelessWidget { diff --git a/examples/flutter_infinite_list/lib/main.dart b/examples/flutter_infinite_list/lib/main.dart index 46774e8b0bc..8c4b1c70d4f 100644 --- a/examples/flutter_infinite_list/lib/main.dart +++ b/examples/flutter_infinite_list/lib/main.dart @@ -1,10 +1,12 @@ import 'package:bloc/bloc.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_infinite_list/app.dart'; import 'package:flutter_infinite_list/simple_bloc_observer.dart'; void main() { - Bloc.observer = SimpleBlocObserver(); - runApp(App()); + BlocOverrides.runZoned( + () => runApp(App()), + blocObserver: SimpleBlocObserver(), + ); } diff --git a/examples/flutter_infinite_list/test/posts/view/posts_list_test.dart b/examples/flutter_infinite_list/test/posts/view/posts_list_test.dart index 01cf25da809..6c09cc47eca 100644 --- a/examples/flutter_infinite_list/test/posts/view/posts_list_test.dart +++ b/examples/flutter_infinite_list/test/posts/view/posts_list_test.dart @@ -5,10 +5,6 @@ import 'package:flutter_infinite_list/posts/posts.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -class FakePostState extends Fake implements PostState {} - -class FakePostEvent extends Fake implements PostEvent {} - class MockPostBloc extends MockBloc implements PostBloc {} extension on WidgetTester { @@ -32,11 +28,6 @@ void main() { late PostBloc postBloc; - setUpAll(() { - registerFallbackValue(FakePostState()); - registerFallbackValue(FakePostEvent()); - }); - setUp(() { postBloc = MockPostBloc(); }); diff --git a/examples/flutter_login/test/login/view/login_form_test.dart b/examples/flutter_login/test/login/view/login_form_test.dart index 0e14a3e8bcf..8d5884a4c4e 100644 --- a/examples/flutter_login/test/login/view/login_form_test.dart +++ b/examples/flutter_login/test/login/view/login_form_test.dart @@ -6,10 +6,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:formz/formz.dart'; import 'package:mocktail/mocktail.dart'; -class FakeLoginEvent extends Fake implements LoginEvent {} - -class FakeLoginState extends Fake implements LoginState {} - class MockLoginBloc extends MockBloc implements LoginBloc {} @@ -17,11 +13,6 @@ void main() { group('LoginForm', () { late LoginBloc loginBloc; - setUpAll(() { - registerFallbackValue(FakeLoginEvent()); - registerFallbackValue(FakeLoginState()); - }); - setUp(() { loginBloc = MockLoginBloc(); }); diff --git a/examples/flutter_shopping_cart/lib/main.dart b/examples/flutter_shopping_cart/lib/main.dart index 68c8c455c16..499d3a6fe7d 100644 --- a/examples/flutter_shopping_cart/lib/main.dart +++ b/examples/flutter_shopping_cart/lib/main.dart @@ -1,10 +1,12 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_shopping_cart/app.dart'; import 'package:flutter_shopping_cart/shopping_repository.dart'; import 'package:flutter_shopping_cart/simple_bloc_observer.dart'; void main() { - Bloc.observer = SimpleBlocObserver(); - runApp(App(shoppingRepository: ShoppingRepository())); + BlocOverrides.runZoned( + () => runApp(App(shoppingRepository: ShoppingRepository())), + blocObserver: SimpleBlocObserver(), + ); } diff --git a/examples/flutter_shopping_cart/test/cart/view/cart_page_test.dart b/examples/flutter_shopping_cart/test/cart/view/cart_page_test.dart index c4dffc93c8d..41864ceec3d 100644 --- a/examples/flutter_shopping_cart/test/cart/view/cart_page_test.dart +++ b/examples/flutter_shopping_cart/test/cart/view/cart_page_test.dart @@ -6,10 +6,6 @@ import 'package:mocktail/mocktail.dart'; import '../../helper.dart'; -class FakeCartState extends Fake implements CartState {} - -class FakeCartEvent extends Fake implements CartEvent {} - void main() { late CartBloc cartBloc; @@ -19,11 +15,6 @@ void main() { Item(3, 'item #3'), ]; - setUpAll(() { - registerFallbackValue(FakeCartState()); - registerFallbackValue(FakeCartEvent()); - }); - setUp(() { cartBloc = MockCartBloc(); }); diff --git a/examples/flutter_shopping_cart/test/catalog/view/catalog_page_test.dart b/examples/flutter_shopping_cart/test/catalog/view/catalog_page_test.dart index 7151c135fcf..04534b90b6a 100644 --- a/examples/flutter_shopping_cart/test/catalog/view/catalog_page_test.dart +++ b/examples/flutter_shopping_cart/test/catalog/view/catalog_page_test.dart @@ -6,25 +6,10 @@ import 'package:mocktail/mocktail.dart'; import '../../helper.dart'; -class FakeCartState extends Fake implements CartState {} - -class FakeCartEvent extends Fake implements CartEvent {} - -class FakeCatalogState extends Fake implements CatalogState {} - -class FakeCatalogEvent extends Fake implements CatalogEvent {} - void main() { late CartBloc cartBloc; late CatalogBloc catalogBloc; - setUpAll(() { - registerFallbackValue(FakeCartState()); - registerFallbackValue(FakeCartEvent()); - registerFallbackValue(FakeCatalogState()); - registerFallbackValue(FakeCatalogEvent()); - }); - setUp(() { catalogBloc = MockCatalogBloc(); cartBloc = MockCartBloc(); diff --git a/examples/flutter_shopping_cart/test/helper.dart b/examples/flutter_shopping_cart/test/helper.dart index 3cd5c42e7a5..17e7567d2e4 100644 --- a/examples/flutter_shopping_cart/test/helper.dart +++ b/examples/flutter_shopping_cart/test/helper.dart @@ -4,15 +4,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class FakeCartState extends Fake implements CartState {} - -class FakeCartEvent extends Fake implements CartEvent {} - -class FakeCatalogState extends Fake implements CatalogState {} - -class FakeCatalogEvent extends Fake implements CatalogEvent {} class MockCartBloc extends MockBloc implements CartBloc {} @@ -25,11 +16,6 @@ extension PumpApp on WidgetTester { CatalogBloc? catalogBloc, required Widget child, }) { - registerFallbackValue(FakeCartState()); - registerFallbackValue(FakeCartEvent()); - registerFallbackValue(FakeCatalogState()); - registerFallbackValue(FakeCatalogEvent()); - return pumpWidget( MaterialApp( home: MultiBlocProvider( diff --git a/examples/flutter_timer/test/timer/view/timer_page_test.dart b/examples/flutter_timer/test/timer/view/timer_page_test.dart index 0ac43069dcc..88ce6219408 100644 --- a/examples/flutter_timer/test/timer/view/timer_page_test.dart +++ b/examples/flutter_timer/test/timer/view/timer_page_test.dart @@ -8,10 +8,6 @@ import 'package:mocktail/mocktail.dart'; class MockTimerBloc extends MockBloc implements TimerBloc {} -class FakeTimerState extends Fake implements TimerState {} - -class FakeTimerEvent extends Fake implements TimerEvent {} - extension on WidgetTester { Future pumpTimerView(TimerBloc timerBloc) { return pumpWidget( @@ -25,11 +21,6 @@ extension on WidgetTester { void main() { late TimerBloc timerBloc; - setUpAll(() { - registerFallbackValue(FakeTimerState()); - registerFallbackValue(FakeTimerEvent()); - }); - setUp(() { timerBloc = MockTimerBloc(); }); diff --git a/examples/flutter_weather/lib/main.dart b/examples/flutter_weather/lib/main.dart index 02f98976811..82d1515b06d 100644 --- a/examples/flutter_weather/lib/main.dart +++ b/examples/flutter_weather/lib/main.dart @@ -8,11 +8,14 @@ import 'package:weather_repository/weather_repository.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - Bloc.observer = WeatherBlocObserver(); - HydratedBloc.storage = await HydratedStorage.build( + final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); - runApp(WeatherApp(weatherRepository: WeatherRepository())); + HydratedBlocOverrides.runZoned( + () => runApp(WeatherApp(weatherRepository: WeatherRepository())), + blocObserver: WeatherBlocObserver(), + storage: storage, + ); } diff --git a/examples/flutter_weather/test/app_test.dart b/examples/flutter_weather/test/app_test.dart index a6c01d31d69..7222e159328 100644 --- a/examples/flutter_weather/test/app_test.dart +++ b/examples/flutter_weather/test/app_test.dart @@ -10,18 +10,11 @@ import 'package:weather_repository/weather_repository.dart'; import 'helpers/hydrated_bloc.dart'; -class FakeColor extends Fake implements Color {} - class MockThemeCubit extends MockCubit implements ThemeCubit {} class MockWeatherRepository extends Mock implements WeatherRepository {} void main() { - setUpAll(() { - initHydratedBloc(); - registerFallbackValue(FakeColor()); - }); - group('WeatherApp', () { late WeatherRepository weatherRepository; @@ -30,7 +23,11 @@ void main() { }); testWidgets('renders WeatherAppView', (tester) async { - await tester.pumpWidget(WeatherApp(weatherRepository: weatherRepository)); + await mockHydratedStorage(() async { + await tester.pumpWidget( + WeatherApp(weatherRepository: weatherRepository), + ); + }); expect(find.byType(WeatherAppView), findsOneWidget); }); }); @@ -46,24 +43,34 @@ void main() { testWidgets('renders WeatherPage', (tester) async { when(() => themeCubit.state).thenReturn(Colors.blue); - await tester.pumpWidget( - RepositoryProvider.value( - value: weatherRepository, - child: BlocProvider.value(value: themeCubit, child: WeatherAppView()), - ), - ); + await mockHydratedStorage(() async { + await tester.pumpWidget( + RepositoryProvider.value( + value: weatherRepository, + child: BlocProvider.value( + value: themeCubit, + child: WeatherAppView(), + ), + ), + ); + }); expect(find.byType(WeatherPage), findsOneWidget); }); testWidgets('has correct theme primary color', (tester) async { const color = Color(0xFFD2D2D2); when(() => themeCubit.state).thenReturn(color); - await tester.pumpWidget( - RepositoryProvider.value( - value: weatherRepository, - child: BlocProvider.value(value: themeCubit, child: WeatherAppView()), - ), - ); + await mockHydratedStorage(() async { + await tester.pumpWidget( + RepositoryProvider.value( + value: weatherRepository, + child: BlocProvider.value( + value: themeCubit, + child: WeatherAppView(), + ), + ), + ); + }); final materialApp = tester.widget(find.byType(MaterialApp)); expect(materialApp.theme?.primaryColor, color); }); diff --git a/examples/flutter_weather/test/helpers/hydrated_bloc.dart b/examples/flutter_weather/test/helpers/hydrated_bloc.dart index 9cadb01149e..faa02eb8b17 100644 --- a/examples/flutter_weather/test/helpers/hydrated_bloc.dart +++ b/examples/flutter_weather/test/helpers/hydrated_bloc.dart @@ -4,12 +4,16 @@ import 'package:mocktail/mocktail.dart'; class MockStorage extends Mock implements Storage {} -late Storage hydratedStorage; +T mockHydratedStorage(T Function() body, {Storage? storage}) { + return HydratedBlocOverrides.runZoned( + body, + storage: storage ?? _buildMockStorage(), + ); +} -void initHydratedBloc() { +Storage _buildMockStorage() { TestWidgetsFlutterBinding.ensureInitialized(); - hydratedStorage = MockStorage(); - when(() => hydratedStorage.write(any(), any())) - .thenAnswer((_) async {}); - HydratedBloc.storage = hydratedStorage; + final storage = MockStorage(); + when(() => storage.write(any(), any())).thenAnswer((_) async {}); + return storage; } diff --git a/examples/flutter_weather/test/settings/view/settings_page_test.dart b/examples/flutter_weather/test/settings/view/settings_page_test.dart index 93f0210b506..5bdc20b9f89 100644 --- a/examples/flutter_weather/test/settings/view/settings_page_test.dart +++ b/examples/flutter_weather/test/settings/view/settings_page_test.dart @@ -7,8 +7,6 @@ import 'package:flutter_weather/settings/settings.dart'; import 'package:flutter_weather/weather/weather.dart'; import 'package:mocktail/mocktail.dart'; -class FakeWeatherState extends Fake implements WeatherState {} - class MockWeatherCubit extends MockCubit implements WeatherCubit { } @@ -16,10 +14,6 @@ void main() { group('SettingsPage', () { late WeatherCubit weatherCubit; - setUpAll(() { - registerFallbackValue(FakeWeatherState()); - }); - setUp(() { weatherCubit = MockWeatherCubit(); }); diff --git a/examples/flutter_weather/test/theme/cubit/theme_cubit_test.dart b/examples/flutter_weather/test/theme/cubit/theme_cubit_test.dart index 04637f39713..7fbd62be780 100644 --- a/examples/flutter_weather/test/theme/cubit/theme_cubit_test.dart +++ b/examples/flutter_weather/test/theme/cubit/theme_cubit_test.dart @@ -18,19 +18,22 @@ class MockWeather extends Mock implements Weather { } void main() { - initHydratedBloc(); group('ThemeCubit', () { test('initial state is correct', () { - expect(ThemeCubit().state, ThemeCubit.defaultColor); + mockHydratedStorage(() { + expect(ThemeCubit().state, ThemeCubit.defaultColor); + }); }); group('toJson/fromJson', () { test('work properly', () { - final themeCubit = ThemeCubit(); - expect( - themeCubit.fromJson(themeCubit.toJson(themeCubit.state)), - themeCubit.state, - ); + mockHydratedStorage(() { + final themeCubit = ThemeCubit(); + expect( + themeCubit.fromJson(themeCubit.toJson(themeCubit.state)), + themeCubit.state, + ); + }); }); }); @@ -43,35 +46,35 @@ void main() { blocTest( 'emits correct color for WeatherCondition.clear', - build: () => ThemeCubit(), + build: () => mockHydratedStorage(() => ThemeCubit()), act: (cubit) => cubit.updateTheme(clearWeather), expect: () => [Colors.orangeAccent], ); blocTest( 'emits correct color for WeatherCondition.snowy', - build: () => ThemeCubit(), + build: () => mockHydratedStorage(() => ThemeCubit()), act: (cubit) => cubit.updateTheme(snowyWeather), expect: () => [Colors.lightBlueAccent], ); blocTest( 'emits correct color for WeatherCondition.cloudy', - build: () => ThemeCubit(), + build: () => mockHydratedStorage(() => ThemeCubit()), act: (cubit) => cubit.updateTheme(cloudyWeather), expect: () => [Colors.blueGrey], ); blocTest( 'emits correct color for WeatherCondition.rainy', - build: () => ThemeCubit(), + build: () => mockHydratedStorage(() => ThemeCubit()), act: (cubit) => cubit.updateTheme(rainyWeather), expect: () => [Colors.indigoAccent], ); blocTest( 'emits correct color for WeatherCondition.unknown', - build: () => ThemeCubit(), + build: () => mockHydratedStorage(() => ThemeCubit()), act: (cubit) => cubit.updateTheme(unknownWeather), expect: () => [ThemeCubit.defaultColor], ); diff --git a/examples/flutter_weather/test/weather/cubit/weather_cubit_test.dart b/examples/flutter_weather/test/weather/cubit/weather_cubit_test.dart index e198278445f..700630c1b52 100644 --- a/examples/flutter_weather/test/weather/cubit/weather_cubit_test.dart +++ b/examples/flutter_weather/test/weather/cubit/weather_cubit_test.dart @@ -22,8 +22,6 @@ void main() { late weather_repository.Weather weather; late weather_repository.WeatherRepository weatherRepository; - setUpAll(initHydratedBloc); - setUp(() { weather = MockWeather(); weatherRepository = MockWeatherRepository(); @@ -36,38 +34,42 @@ void main() { }); test('initial state is correct', () { - final weatherCubit = WeatherCubit(weatherRepository); - expect(weatherCubit.state, WeatherState()); + mockHydratedStorage(() { + final weatherCubit = WeatherCubit(weatherRepository); + expect(weatherCubit.state, WeatherState()); + }); }); group('toJson/fromJson', () { test('work properly', () { - final weatherCubit = WeatherCubit(weatherRepository); - expect( - weatherCubit.fromJson(weatherCubit.toJson(weatherCubit.state)), - weatherCubit.state, - ); + mockHydratedStorage(() { + final weatherCubit = WeatherCubit(weatherRepository); + expect( + weatherCubit.fromJson(weatherCubit.toJson(weatherCubit.state)), + weatherCubit.state, + ); + }); }); }); group('fetchWeather', () { blocTest( 'emits nothing when city is null', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), act: (cubit) => cubit.fetchWeather(null), expect: () => [], ); blocTest( 'emits nothing when city is empty', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), act: (cubit) => cubit.fetchWeather(''), expect: () => [], ); blocTest( 'calls getWeather with correct city', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), act: (cubit) => cubit.fetchWeather(weatherLocation), verify: (_) { verify(() => weatherRepository.getWeather(weatherLocation)).called(1); @@ -81,7 +83,7 @@ void main() { () => weatherRepository.getWeather(any()), ).thenThrow(Exception('oops')); }, - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), act: (cubit) => cubit.fetchWeather(weatherLocation), expect: () => [ WeatherState(status: WeatherStatus.loading), @@ -91,7 +93,7 @@ void main() { blocTest( 'emits [loading, success] when getWeather returns (celsius)', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), act: (cubit) => cubit.fetchWeather(weatherLocation), expect: () => [ WeatherState(status: WeatherStatus.loading), @@ -115,7 +117,7 @@ void main() { blocTest( 'emits [loading, success] when getWeather returns (fahrenheit)', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), seed: () => WeatherState(temperatureUnits: TemperatureUnits.fahrenheit), act: (cubit) => cubit.fetchWeather(weatherLocation), expect: () => [ @@ -145,7 +147,7 @@ void main() { group('refreshWeather', () { blocTest( 'emits nothing when status is not success', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), act: (cubit) => cubit.refreshWeather(), expect: () => [], verify: (_) { @@ -155,7 +157,7 @@ void main() { blocTest( 'emits nothing when location is null', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), seed: () => WeatherState(status: WeatherStatus.success), act: (cubit) => cubit.refreshWeather(), expect: () => [], @@ -166,7 +168,7 @@ void main() { blocTest( 'invokes getWeather with correct location', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), seed: () => WeatherState( status: WeatherStatus.success, weather: Weather( @@ -189,7 +191,7 @@ void main() { () => weatherRepository.getWeather(any()), ).thenThrow(Exception('oops')); }, - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), seed: () => WeatherState( status: WeatherStatus.success, weather: Weather( @@ -205,7 +207,7 @@ void main() { blocTest( 'emits updated weather (celsius)', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), seed: () => WeatherState( status: WeatherStatus.success, weather: Weather( @@ -237,7 +239,7 @@ void main() { blocTest( 'emits updated weather (fahrenheit)', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), seed: () => WeatherState( temperatureUnits: TemperatureUnits.fahrenheit, status: WeatherStatus.success, @@ -272,7 +274,7 @@ void main() { group('toggleUnits', () { blocTest( 'emits updated units when status is not success', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), act: (cubit) => cubit.toggleUnits(), expect: () => [ WeatherState(temperatureUnits: TemperatureUnits.fahrenheit), @@ -282,7 +284,7 @@ void main() { blocTest( 'emits updated units and temperature ' 'when status is success (celsius)', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), seed: () => WeatherState( status: WeatherStatus.success, temperatureUnits: TemperatureUnits.fahrenheit, @@ -311,7 +313,7 @@ void main() { blocTest( 'emits updated units and temperature ' 'when status is success (fahrenheit)', - build: () => WeatherCubit(weatherRepository), + build: () => mockHydratedStorage(() => WeatherCubit(weatherRepository)), seed: () => WeatherState( status: WeatherStatus.success, temperatureUnits: TemperatureUnits.celsius, diff --git a/examples/flutter_weather/test/weather/view/weather_page_test.dart b/examples/flutter_weather/test/weather/view/weather_page_test.dart index f7f2e7e86b8..dfd7802d961 100644 --- a/examples/flutter_weather/test/weather/view/weather_page_test.dart +++ b/examples/flutter_weather/test/weather/view/weather_page_test.dart @@ -15,22 +15,12 @@ import '../../helpers/hydrated_bloc.dart'; class MockWeatherRepository extends Mock implements WeatherRepository {} -class FakeColor extends Fake implements Color {} - class MockThemeCubit extends MockCubit implements ThemeCubit {} -class FakeWeatherState extends Fake implements WeatherState {} - class MockWeatherCubit extends MockCubit implements WeatherCubit { } void main() { - setUpAll(() { - initHydratedBloc(); - registerFallbackValue(FakeColor()); - registerFallbackValue(FakeWeatherState()); - }); - group('WeatherPage', () { late WeatherRepository weatherRepository; @@ -39,10 +29,12 @@ void main() { }); testWidgets('renders WeatherView', (tester) async { - await tester.pumpWidget(RepositoryProvider.value( - value: weatherRepository, - child: MaterialApp(home: WeatherPage()), - )); + await mockHydratedStorage(() async { + await tester.pumpWidget(RepositoryProvider.value( + value: weatherRepository, + child: MaterialApp(home: WeatherPage()), + )); + }); expect(find.byType(WeatherView), findsOneWidget); }); }); @@ -110,17 +102,20 @@ void main() { }); testWidgets('state is cached', (tester) async { - when(() => hydratedStorage.read('WeatherCubit')).thenReturn( + final storage = MockStorage(); + when(() => storage.read('WeatherCubit')).thenReturn( WeatherState( status: WeatherStatus.success, weather: weather, temperatureUnits: TemperatureUnits.fahrenheit, ).toJson(), ); - await tester.pumpWidget(BlocProvider.value( - value: WeatherCubit(MockWeatherRepository()), - child: MaterialApp(home: WeatherView()), - )); + await mockHydratedStorage(() async { + await tester.pumpWidget(BlocProvider.value( + value: WeatherCubit(MockWeatherRepository()), + child: MaterialApp(home: WeatherView()), + )); + }, storage: storage); expect(find.byType(WeatherPopulated), findsOneWidget); }); diff --git a/packages/angular_bloc/CHANGELOG.md b/packages/angular_bloc/CHANGELOG.md index 1135ddb609b..1265008e1f1 100644 --- a/packages/angular_bloc/CHANGELOG.md +++ b/packages/angular_bloc/CHANGELOG.md @@ -1,3 +1,15 @@ +# 8.0.0-dev.3 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.5` + +# 8.0.0-dev.2 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.3` + +# 8.0.0-dev.1 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.2` + # 7.1.0 - feat: upgrade to `bloc ^7.2.0` diff --git a/packages/angular_bloc/README.md b/packages/angular_bloc/README.md index a096d19732b..bb345bca9c4 100644 --- a/packages/angular_bloc/README.md +++ b/packages/angular_bloc/README.md @@ -175,8 +175,8 @@ At this point we have successfully separated our presentational layer from our b ## Examples -- [Counter](https://github.com/felangel/Bloc/tree/master/examples/angular_counter) - a complete example of how to create a `CounterBloc` and hook it up to an AngularDart app. -- [Github Search](https://github.com/felangel/Bloc/tree/master/examples/github_search/angular_github_search) - an example of how to create a Github Search Application using the `bloc` and `angular_bloc` packages. +- [Counter](https://github.com/felangel/bloc/tree/master/examples/angular_counter) - a complete example of how to create a `CounterBloc` and hook it up to an AngularDart app. +- [Github Search](https://github.com/felangel/bloc/tree/master/examples/github_search/angular_github_search) - an example of how to create a Github Search Application using the `bloc` and `angular_bloc` packages. ## Maintainers diff --git a/packages/angular_bloc/lib/angular_bloc.dart b/packages/angular_bloc/lib/angular_bloc.dart index 76a8aa17cc7..a409859feee 100644 --- a/packages/angular_bloc/lib/angular_bloc.dart +++ b/packages/angular_bloc/lib/angular_bloc.dart @@ -5,5 +5,4 @@ library angular_dart; export 'package:bloc/bloc.dart'; - export './src/pipes/pipes.dart'; diff --git a/packages/angular_bloc/pubspec.yaml b/packages/angular_bloc/pubspec.yaml index 89204d14a3f..029990785e3 100644 --- a/packages/angular_bloc/pubspec.yaml +++ b/packages/angular_bloc/pubspec.yaml @@ -1,6 +1,6 @@ name: angular_bloc description: Angular Components that make it easy to implement the BLoC (Business Logic Component) design pattern. Built to be used with the bloc state management package. -version: 7.1.0 +version: 8.0.0-dev.3 repository: https://github.com/felangel/bloc/tree/master/packages/angular_bloc issue_tracker: https://github.com/felangel/bloc/issues homepage: https://bloclibrary.dev @@ -11,7 +11,7 @@ environment: dependencies: angular: ^7.0.0 - bloc: ^7.2.0 + bloc: ^8.0.0-dev.5 dev_dependencies: angular_test: ^4.0.0 @@ -19,5 +19,5 @@ dev_dependencies: build_test: ^2.0.0 build_web_compilers: ^3.0.0 meta: ^1.2.3 - mocktail: ^0.1.4 - test: ^1.14.6 + mocktail: ^0.2.0 + test: ^1.16.0 diff --git a/packages/angular_bloc/test/bloc_pipe_test.dart b/packages/angular_bloc/test/bloc_pipe_test.dart index e88a0b52643..61540cd2c74 100644 --- a/packages/angular_bloc/test/bloc_pipe_test.dart +++ b/packages/angular_bloc/test/bloc_pipe_test.dart @@ -38,6 +38,7 @@ void main() { test('should return initialState when subscribing to an bloc', () { expect(pipe.transform(bloc), 0); }); + test('should return the latest available value', () async { pipe.transform(bloc); bloc.add(Increment()); @@ -70,6 +71,7 @@ void main() { expect(pipe.transform(newBloc), 0); })); }); + test('should not dispose of existing subscription when Streams are equal', () async { // See https://github.com/dart-lang/angular2/issues/260 @@ -80,6 +82,7 @@ void main() { expect(pipe.transform(_bloc), 1); })); }); + test('should request a change detection check upon receiving a new value', () async { pipe.transform(bloc); @@ -89,11 +92,13 @@ void main() { })); }); }); + group('ngOnDestroy', () { test('should do nothing when no subscription and not throw exception', () { pipe.ngOnDestroy(); }); + test('should dispose of the existing subscription', () async { pipe ..transform(bloc) diff --git a/packages/angular_bloc/test/legacy/bloc_pipe_test.dart b/packages/angular_bloc/test/legacy/bloc_pipe_test.dart deleted file mode 100644 index 7929e45cc90..00000000000 --- a/packages/angular_bloc/test/legacy/bloc_pipe_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -@TestOn('browser') - -import 'dart:async'; - -import 'package:angular/angular.dart' show ChangeDetectorRef; -import 'package:angular_bloc/angular_bloc.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:test/test.dart'; - -class MockChangeDetectorRef extends Mock implements ChangeDetectorRef {} - -enum CounterEvent { increment, decrement } - -class CounterBloc extends Bloc { - CounterBloc() : super(0); - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.decrement: - yield state - 1; - break; - case CounterEvent.increment: - yield state + 1; - break; - } - } -} - -void main() { - group('Stream', () { - late Bloc bloc; - late BlocPipe pipe; - late ChangeDetectorRef ref; - - setUp(() { - bloc = CounterBloc(); - ref = MockChangeDetectorRef(); - pipe = BlocPipe(ref); - }); - - group('transform', () { - test('should return initialState when subscribing to an bloc', () { - expect(pipe.transform(bloc), 0); - }); - test('should return the latest available value', () async { - pipe.transform(bloc); - bloc.add(CounterEvent.increment); - Timer.run(expectAsync0(() { - final dynamic res = pipe.transform(bloc); - expect(res, 1); - })); - }); - - test( - 'should return same value when nothing has changed ' - 'since the last call', () async { - pipe.transform(bloc); - bloc.add(CounterEvent.increment); - Timer.run(expectAsync0(() { - pipe.transform(bloc); - expect(pipe.transform(bloc), 1); - })); - }); - - test( - 'should dispose of the existing subscription when ' - 'subscribing to a new bloc', () async { - pipe.transform(bloc); - var newBloc = CounterBloc(); - expect(pipe.transform(newBloc), 0); - // this should not affect the pipe - bloc.add(CounterEvent.increment); - Timer.run(expectAsync0(() { - expect(pipe.transform(newBloc), 0); - })); - }); - test('should not dispose of existing subscription when Streams are equal', - () async { - // See https://github.com/dart-lang/angular2/issues/260 - final _bloc = CounterBloc(); - expect(pipe.transform(_bloc), 0); - _bloc.add(CounterEvent.increment); - Timer.run(expectAsync0(() { - expect(pipe.transform(_bloc), 1); - })); - }); - test('should request a change detection check upon receiving a new value', - () async { - pipe.transform(bloc); - bloc.add(CounterEvent.increment); - Timer(const Duration(milliseconds: 10), expectAsync0(() { - verify(() => ref.markForCheck()).called(1); - })); - }); - }); - group('ngOnDestroy', () { - test('should do nothing when no subscription and not throw exception', - () { - pipe.ngOnDestroy(); - }); - test('should dispose of the existing subscription', () async { - pipe - ..transform(bloc) - ..ngOnDestroy(); - bloc.add(CounterEvent.increment); - Timer.run(expectAsync0(() { - expect(pipe.transform(bloc), 1); - })); - }); - }); - }); -} diff --git a/packages/bloc/CHANGELOG.md b/packages/bloc/CHANGELOG.md index 4ccac0d4608..ca9f9b73cc5 100644 --- a/packages/bloc/CHANGELOG.md +++ b/packages/bloc/CHANGELOG.md @@ -1,3 +1,37 @@ +# 8.0.0-dev.5 + +- **BREAKING**: feat: introduce `BlocOverrides` API ([#2932](https://github.com/felangel/bloc/pull/2932)) + - `Bloc.observer` removed in favor of `BlocOverrides.runZoned` and `BlocOverrides.current.blocObserver` + - `Bloc.transformer` removed in favor of `BlocOverrides.runZoned` and `BlocOverrides.current.eventTransformer` +- **BREAKING**: refactor: make `BlocObserver` an abstract class +- **BREAKING**: feat: `add` throws `StateError` when bloc is closed ([#2912](https://github.com/felangel/bloc/pull/2912)) +- **BREAKING**: feat: `emit` throws `StateError` when bloc is closed ([#2913](https://github.com/felangel/bloc/pull/2913)) + +# 8.0.0-dev.4 + +- **BREAKING**: feat: improve error handling/reporting + - `BlocUnhandledErrorException` is removed + - Uncaught exceptions are always reported to `onError` and rethrown + - `addError` reports error to `onError` but does not propagate as an uncaught exception + +# 8.0.0-dev.3 + +- **BREAKING**: feat: restrict scope of `emit` in `Bloc` and `Cubit` + - In `Cubit`, `emit` is `protected` so it can only be used within the `Cubit` instance. + - In `Bloc`, `emit` is `internal` so it cannot be used outside of the internal package implementation. + +# 8.0.0-dev.2 + +- **BREAKING**: refactor: remove deprecated `listen` on `BlocBase` + +# 8.0.0-dev.1 + +- **BREAKING**: refactor: remove deprecated `TransitionFunction` +- **BREAKING**: refactor: remove deprecated `transformEvents` +- **BREAKING**: refactor: remove deprecated `mapEventToState` +- **BREAKING**: refactor: remove deprecated `transformTransitions` +- feat: throw `StateError` if an event is added without a registered event handler + # 7.2.1 - fix: `on` should have an `EventTransformer` instead of `EventTransformer` @@ -24,7 +58,7 @@ # 7.2.0-dev.3 -- **BREAKING**: refactor!: require `emit.forEach` `onData` to be synchronous +- **BREAKING**: refactor: require `emit.forEach` `onData` to be synchronous - refactor: minor internal optimizations in `on` implementation # 7.2.0-dev.2 diff --git a/packages/bloc/README.md b/packages/bloc/README.md index f636fba540d..18760d58134 100644 --- a/packages/bloc/README.md +++ b/packages/bloc/README.md @@ -166,8 +166,12 @@ class MyBlocObserver extends BlocObserver { ```dart void main() { - Bloc.observer = MyBlocObserver(); - // Use cubits... + BlocOverrides.runZoned( + () { + // Use cubits... + }, + blocObserver: MyBlocObserver(), + ); } ``` @@ -317,8 +321,12 @@ class MyBlocObserver extends BlocObserver { ```dart void main() { - Bloc.observer = MyBlocObserver(); - // Use blocs... + BlocOverrides.runZoned( + () { + // Use blocs... + }, + blocObserver: MyBlocObserver(), + ); } ``` @@ -328,7 +336,7 @@ void main() { ## Examples -- [Counter](https://github.com/felangel/Bloc/tree/master/packages/bloc/example) - an example of how to create a `CounterBloc` in a pure Dart app. +- [Counter](https://github.com/felangel/bloc/tree/master/packages/bloc/example) - an example of how to create a `CounterBloc` in a pure Dart app. ## Maintainers diff --git a/packages/bloc/example/main.dart b/packages/bloc/example/main.dart index 8423c6fcbc1..2a3566c75e6 100644 --- a/packages/bloc/example/main.dart +++ b/packages/bloc/example/main.dart @@ -41,10 +41,10 @@ class SimpleBlocObserver extends BlocObserver { } void main() { - Bloc.observer = SimpleBlocObserver(); - - cubitMain(); - blocMain(); + BlocOverrides.runZoned(() { + cubitMain(); + blocMain(); + }, blocObserver: SimpleBlocObserver()); } void cubitMain() { diff --git a/packages/bloc/lib/src/bloc.dart b/packages/bloc/lib/src/bloc.dart index 12b03fa38e9..ee1ba916c0e 100644 --- a/packages/bloc/lib/src/bloc.dart +++ b/packages/bloc/lib/src/bloc.dart @@ -1,9 +1,104 @@ -// ignore_for_file: deprecated_member_use_from_same_package import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; +const _asyncRunZoned = runZoned; + +/// This class facilitates overriding [BlocObserver] and [EventTransformer]. +/// It should be extended by another class in client code with overrides +/// that construct a custom implementation. The implementation in this class +/// defaults to the base [blocObserver] and [eventTransformer] implementation. +/// For example: +/// +/// ```dart +/// class MyBlocObserver extends BlocObserver { +/// ... +/// // A custom BlocObserver implementation. +/// ... +/// } +/// +/// void main() { +/// BlocOverrides.runZoned(() { +/// ... +/// // Bloc instances will use MyBlocObserver instead of the default BlocObserver. +/// ... +/// }, blocObserver: MyBlocObserver()); +/// } +/// ``` +abstract class BlocOverrides { + static final _token = Object(); + + /// Returns the current [BlocOverrides] instance. + /// + /// This will return `null` if the current [Zone] does not contain + /// any [BlocOverrides]. + /// + /// See also: + /// * [BlocOverrides.runZoned] to provide [BlocOverrides] in a fresh [Zone]. + /// + static BlocOverrides? get current => Zone.current[_token] as BlocOverrides?; + + /// Runs [body] in a fresh [Zone] using the provided overrides. + static R runZoned( + R Function() body, { + BlocObserver? blocObserver, + EventTransformer? eventTransformer, + }) { + final overrides = _BlocOverridesScope(blocObserver, eventTransformer); + return _asyncRunZoned(body, zoneValues: {_token: overrides}); + } + + /// The [BlocObserver] that will be used within the current [Zone]. + /// + /// By default, a base [BlocObserver] implementation is used. + BlocObserver get blocObserver => _defaultBlocObserver; + + /// The [EventTransformer] that will be used within the current [Zone]. + /// + /// By default, all events are processed concurrently. + /// + /// If a custom transformer is specified for a particular event handler, + /// it will take precendence over the global transformer. + /// + /// See also: + /// + /// * [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) for an + /// opinionated set of event transformers. + /// + EventTransformer get eventTransformer => _defaultEventTransformer; +} + +class _BlocOverridesScope extends BlocOverrides { + _BlocOverridesScope(this._blocObserver, this._eventTransformer); + + final BlocOverrides? _previous = BlocOverrides.current; + final BlocObserver? _blocObserver; + final EventTransformer? _eventTransformer; + + @override + BlocObserver get blocObserver { + final blocObserver = _blocObserver; + if (blocObserver != null) return blocObserver; + + final previous = _previous; + if (previous != null) return previous.blocObserver; + + return super.blocObserver; + } + + @override + EventTransformer get eventTransformer { + final eventTransformer = _eventTransformer; + if (eventTransformer != null) return eventTransformer; + + final previous = _previous; + if (previous != null) return previous.eventTransformer; + + return super.eventTransformer; + } +} + /// {@template emitter} /// An [Emitter] is a class which is capable of emitting new states. /// @@ -87,7 +182,7 @@ typedef EventTransformer = Stream Function( class _Emitter implements Emitter { _Emitter(this._emit); - final void Function(State) _emit; + final void Function(State state) _emit; final _completer = Completer(); final _disposables = Function()>[]; @@ -97,7 +192,7 @@ class _Emitter implements Emitter { @override Future onEach( Stream stream, { - required void Function(T) onData, + required void Function(T data) onData, void Function(Object error, StackTrace stackTrace)? onError, }) async { final completer = Completer(); @@ -117,7 +212,7 @@ class _Emitter implements Emitter { @override Future forEach( Stream stream, { - required State Function(T) onData, + required State Function(T data) onData, State Function(Object error, StackTrace stackTrace)? onError, }) { return onEach( @@ -207,46 +302,10 @@ Please make sure to await all asynchronous operations within event handlers. Future get future => _completer.future; } -/// **@Deprecated - Use `on` with an `EventTransformer` instead. -/// Will be removed in v8.0.0** -/// -/// Signature for a mapper function which takes an [Event] as input -/// and outputs a [Stream] of [Transition] objects. -@Deprecated( - 'Use `on` with an `EventTransformer` instead. ' - 'Will be removed in v8.0.0', -) -typedef TransitionFunction = Stream> - Function(Event); - -/// {@template bloc_unhandled_error_exception} -/// Exception thrown when an unhandled error occurs within a bloc. -/// -/// _Note: thrown in debug mode only_ -/// {@endtemplate} -class BlocUnhandledErrorException implements Exception { - /// {@macro bloc_unhandled_error_exception} - BlocUnhandledErrorException( - this.bloc, - this.error, [ - this.stackTrace = StackTrace.empty, - ]); - - /// The bloc in which the unhandled error occurred. - final BlocBase bloc; - - /// The unhandled [error] object. - final Object error; - - /// Stack trace which accompanied the error. - /// May be [StackTrace.empty] if no stack trace was provided. - final StackTrace stackTrace; - - @override - String toString() { - return 'Unhandled error $error occurred in $bloc.\n' - '$stackTrace'; - } +class _Handler { + const _Handler({required this.isType, required this.type}); + final bool Function(dynamic value) isType; + final Type type; } /// {@template bloc} @@ -255,48 +314,41 @@ class BlocUnhandledErrorException implements Exception { /// {@endtemplate} abstract class Bloc extends BlocBase { /// {@macro bloc} - Bloc(State initialState) : super(initialState) { - _bindEventsToStates(); - } - - /// The current [BlocObserver] instance. - static BlocObserver observer = BlocObserver(); - - /// The default [EventTransformer] used for all event handlers. - /// By default all events are processed concurrently. - /// - /// If a custom transformer is specified for a particular event handler, - /// it will take precendence over the global transformer. - /// - /// See also: - /// - /// * [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) for an - /// opinionated set of event transformers. - /// - static EventTransformer transformer = (events, mapper) { - return events - .map(mapper) - .transform(const _FlatMapStreamTransformer()); - }; - - StreamSubscription>? _transitionSubscription; + Bloc(State initialState) : super(initialState); final _eventController = StreamController.broadcast(); final _subscriptions = >[]; - final _handlerTypes = []; + final _handlers = <_Handler>[]; final _emitters = <_Emitter>[]; + final _eventTransformer = + BlocOverrides.current?.eventTransformer ?? _defaultEventTransformer; /// Notifies the [Bloc] of a new [event] which triggers /// all corresponding [EventHandler] instances. - /// If [close] has already been called, any subsequent calls to [add] will - /// be ignored and will not result in any subsequent state changes. + /// + /// * A [StateError] will be thrown if there is no event handler + /// registered for the incoming [event]. + /// + /// * A [StateError] will be thrown if the bloc is closed and the + /// [event] will not be processed. void add(Event event) { - if (_eventController.isClosed) return; + assert(() { + final handlerExists = _handlers.any((handler) => handler.isType(event)); + if (!handlerExists) { + final eventType = event.runtimeType; + throw StateError( + '''add($eventType) was called without a registered event handler.\n''' + '''Make sure to register a handler via on<$eventType>((event, emit) {...})''', + ); + } + return true; + }()); try { onEvent(event); _eventController.add(event); } catch (error, stackTrace) { onError(error, stackTrace); + rethrow; } } @@ -322,63 +374,30 @@ abstract class Bloc extends BlocBase { @mustCallSuper void onEvent(Event event) { // ignore: invalid_use_of_protected_member - observer.onEvent(this, event); + _blocObserver?.onEvent(this, event); } - /// **@Deprecated - Use `on` with an `EventTransformer` instead. - /// Will be removed in v8.0.0** - /// - /// Transforms the [events] stream along with a [transitionFn] function into - /// a `Stream`. - /// Events that should be processed by [mapEventToState] need to be passed to - /// [transitionFn]. - /// By default `asyncExpand` is used to ensure all [events] are processed in - /// the order in which they are received. - /// You can override [transformEvents] for advanced usage in order to - /// manipulate the frequency and specificity with which [mapEventToState] is - /// called as well as which [events] are processed. - /// - /// For example, if you only want [mapEventToState] to be called on the most - /// recent [Event] you can use `switchMap` instead of `asyncExpand`. - /// - /// ```dart - /// @override - /// Stream> transformEvents(events, transitionFn) { - /// return events.switchMap(transitionFn); - /// } - /// ``` - /// - /// Alternatively, if you only want [mapEventToState] to be called for - /// distinct [events]: + /// {@template emit} + /// **[emit] is only for internal use and should never be called directly. + /// The [Emitter] instance provided to each [EventHandler] + /// should be used instead.** /// /// ```dart - /// @override - /// Stream> transformEvents(events, transitionFn) { - /// return super.transformEvents( - /// events.distinct(), - /// transitionFn, - /// ); + /// class MyBloc extends Bloc { + /// MyBloc() : super(MyInitialState()) { + /// on((event, emit) { + /// // use `emit` to update the state. + /// emit(MyOtherState()); + /// }); + /// } /// } /// ``` - @Deprecated( - 'Use `on` with an `EventTransformer` instead. ' - 'Will be removed in v8.0.0', - ) - Stream> transformEvents( - Stream events, - TransitionFunction transitionFn, - ) { - return events.asyncExpand(transitionFn); - } - - /// {@template emit} - /// **[emit] should never be used outside of tests.** /// /// Updates the state of the bloc to the provided [state]. /// A bloc's state should only be updated by `emitting` a new `state` /// from an [EventHandler] in response to an incoming event. /// {@endtemplate} - @visibleForTesting + @internal @override void emit(State state) => super.emit(state); @@ -412,18 +431,18 @@ abstract class Bloc extends BlocBase { EventTransformer? transformer, }) { assert(() { - final handlerExists = _handlerTypes.any((type) => type == E); + final handlerExists = _handlers.any((handler) => handler.type == E); if (handlerExists) { throw StateError( 'on<$E> was called multiple times. ' 'There should only be a single event handler per event type.', ); } - _handlerTypes.add(E); + _handlers.add(_Handler(isType: (dynamic e) => e is E, type: E)); return true; }()); - final _transformer = transformer ?? Bloc.transformer; + final _transformer = transformer ?? _eventTransformer; final subscription = _transformer( _eventController.stream.where((event) => event is E).cast(), (dynamic event) { @@ -456,6 +475,7 @@ abstract class Bloc extends BlocBase { await handler(event as E, emitter); } catch (error, stackTrace) { onError(error, stackTrace); + rethrow; } finally { onDone(); } @@ -468,15 +488,6 @@ abstract class Bloc extends BlocBase { _subscriptions.add(subscription); } - /// **@Deprecated - Use on instead. Will be removed in v8.0.0** - /// - /// Must be implemented when a class extends [Bloc]. - /// [mapEventToState] is called whenever an [event] is [add]ed - /// and is responsible for converting that [event] into a new [state]. - /// [mapEventToState] can `yield` zero, one, or multiple states for an event. - @Deprecated('Use on instead. Will be removed in v8.0.0') - Stream mapEventToState(Event event) async* {} - /// Called whenever a [transition] occurs with the given [transition]. /// A [transition] occurs when a new `event` is added /// and a new state is `emitted` from a corresponding [EventHandler]. @@ -503,36 +514,7 @@ abstract class Bloc extends BlocBase { @mustCallSuper void onTransition(Transition transition) { // ignore: invalid_use_of_protected_member - Bloc.observer.onTransition(this, transition); - } - - /// **@Deprecated - Override `Stream get stream` instead. - /// Will be removed in v8.0.0** - /// - /// Transforms the `Stream` into a new `Stream`. - /// By default [transformTransitions] returns - /// the incoming `Stream`. - /// You can override [transformTransitions] for advanced usage in order to - /// manipulate the frequency and specificity at which `transitions` - /// (state changes) occur. - /// - /// For example, if you want to debounce outgoing state changes: - /// - /// ```dart - /// @override - /// Stream> transformTransitions( - /// Stream> transitions, - /// ) { - /// return transitions.debounceTime(Duration(seconds: 1)); - /// } - /// ``` - @Deprecated( - 'Override `Stream get stream` instead. Will be removed in v8.0.0', - ) - Stream> transformTransitions( - Stream> transitions, - ) { - return transitions; + _blocObserver?.onTransition(this, transition); } /// Closes the `event` and `state` `Streams`. @@ -548,48 +530,8 @@ abstract class Bloc extends BlocBase { for (final emitter in _emitters) emitter.cancel(); await Future.wait(_emitters.map((e) => e.future)); await Future.wait(_subscriptions.map((s) => s.cancel())); - await _transitionSubscription?.cancel(); return super.close(); } - - void _bindEventsToStates() { - void assertNoMixedUsage() { - assert(() { - if (_handlerTypes.isNotEmpty) { - throw StateError( - 'mapEventToState cannot be overridden in ' - 'conjunction with on.', - ); - } - return true; - }()); - } - - _transitionSubscription = transformTransitions( - transformEvents( - _eventController.stream, - (event) => mapEventToState(event).map( - (nextState) => Transition( - currentState: state, - event: event, - nextState: nextState, - ), - ), - ), - ).listen( - (transition) { - if (transition.nextState == state && _emitted) return; - try { - assertNoMixedUsage(); - onTransition(transition); - emit(transition.nextState); - } catch (error, stackTrace) { - onError(error, stackTrace); - } - }, - onError: onError, - ); - } } /// {@template cubit} @@ -623,9 +565,11 @@ abstract class BlocBase { /// {@macro bloc_stream} BlocBase(this._state) { // ignore: invalid_use_of_protected_member - Bloc.observer.onCreate(this); + _blocObserver?.onCreate(this); } + final _blocObserver = BlocOverrides.current?.blocObserver; + StreamController? __stateController; StreamController get _stateController { return __stateController ??= StreamController.broadcast(); @@ -647,41 +591,30 @@ abstract class BlocBase { /// Subsequent state changes cannot occur within a closed bloc. bool get isClosed => _stateController.isClosed; - /// Adds a subscription to the `Stream`. - /// Returns a [StreamSubscription] which handles events from - /// the `Stream` using the provided [onData], [onError] and [onDone] - /// handlers. - @Deprecated( - 'Use stream.listen instead. Will be removed in v8.0.0', - ) - StreamSubscription listen( - void Function(State)? onData, { - Function? onError, - void Function()? onDone, - bool? cancelOnError, - }) { - return stream.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ); - } - /// Updates the [state] to the provided [state]. - /// [emit] does nothing if the instance has been closed or if the - /// [state] being emitted is equal to the current [state]. + /// [emit] does nothing if the [state] being emitted + /// is equal to the current [state]. /// /// To allow for the possibility of notifying listeners of the initial state, /// emitting a state which is equal to the initial state is allowed as long /// as it is the first thing emitted by the instance. + /// + /// * Throws a [StateError] if the bloc is closed. + @protected void emit(State state) { - if (_stateController.isClosed) return; - if (state == _state && _emitted) return; - onChange(Change(currentState: this.state, nextState: state)); - _state = state; - _stateController.add(_state); - _emitted = true; + try { + if (isClosed) { + throw StateError('Cannot emit new states after calling close'); + } + if (state == _state && _emitted) return; + onChange(Change(currentState: this.state, nextState: state)); + _state = state; + _stateController.add(_state); + _emitted = true; + } catch (error, stackTrace) { + onError(error, stackTrace); + rethrow; + } } /// Called whenever a [change] occurs with the given [change]. @@ -707,7 +640,7 @@ abstract class BlocBase { @mustCallSuper void onChange(Change change) { // ignore: invalid_use_of_protected_member - Bloc.observer.onChange(this, change); + _blocObserver?.onChange(this, change); } /// Reports an [error] which triggers [onError] with an optional [StackTrace]. @@ -718,13 +651,8 @@ abstract class BlocBase { /// Called whenever an [error] occurs and notifies [BlocObserver.onError]. /// - /// In debug mode, [onError] throws a [BlocUnhandledErrorException] for - /// improved visibility. - /// - /// In release mode, [onError] does not throw and will instead only report - /// the error to [BlocObserver.onError]. - /// /// **Note: `super.onError` should always be called last.** + /// /// ```dart /// @override /// void onError(Object error, StackTrace stackTrace) { @@ -738,10 +666,7 @@ abstract class BlocBase { @mustCallSuper void onError(Object error, StackTrace stackTrace) { // ignore: invalid_use_of_protected_member - Bloc.observer.onError(this, error, stackTrace); - assert(() { - throw BlocUnhandledErrorException(this, error, stackTrace); - }()); + _blocObserver?.onError(this, error, stackTrace); } /// Closes the instance. @@ -750,11 +675,20 @@ abstract class BlocBase { @mustCallSuper Future close() async { // ignore: invalid_use_of_protected_member - Bloc.observer.onClose(this); + _blocObserver?.onClose(this); await _stateController.close(); } } +late final _defaultBlocObserver = _DefaultBlocObserver(); +late final _defaultEventTransformer = (Stream events, EventMapper mapper) { + return events + .map(mapper) + .transform(const _FlatMapStreamTransformer()); +}; + +class _DefaultBlocObserver extends BlocObserver {} + class _FlatMapStreamTransformer extends StreamTransformerBase, T> { const _FlatMapStreamTransformer(); diff --git a/packages/bloc/lib/src/bloc_observer.dart b/packages/bloc/lib/src/bloc_observer.dart index 53c9871cb15..05689c44523 100644 --- a/packages/bloc/lib/src/bloc_observer.dart +++ b/packages/bloc/lib/src/bloc_observer.dart @@ -2,7 +2,7 @@ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; /// An interface for observing the behavior of [Bloc] instances. -class BlocObserver { +abstract class BlocObserver { /// Called whenever a [Bloc] is instantiated. /// In many cases, a cubit may be lazily instantiated and /// [onCreate] can be used to observe exactly when the cubit diff --git a/packages/bloc/pubspec.yaml b/packages/bloc/pubspec.yaml index 575ef4b36f8..36724fd7f16 100644 --- a/packages/bloc/pubspec.yaml +++ b/packages/bloc/pubspec.yaml @@ -1,6 +1,6 @@ name: bloc description: A predictable state management library that helps implement the BLoC (Business Logic Component) design pattern. -version: 7.2.1 +version: 8.0.0-dev.5 repository: https://github.com/felangel/bloc/tree/master/packages/bloc issue_tracker: https://github.com/felangel/bloc/issues homepage: https://github.com/felangel/bloc @@ -13,6 +13,6 @@ dependencies: meta: ^1.3.0 dev_dependencies: - mocktail: ^0.1.0 + mocktail: ^0.2.0 stream_transform: ^2.0.0 - test: ^1.16.0 + test: ^1.18.2 diff --git a/packages/bloc/test/bloc_event_transformer_test.dart b/packages/bloc/test/bloc_event_transformer_test.dart index f850aa42e4d..7ad003cc799 100644 --- a/packages/bloc/test/bloc_event_transformer_test.dart +++ b/packages/bloc/test/bloc_event_transformer_test.dart @@ -39,16 +39,6 @@ class CounterBloc extends Bloc { } void main() { - late EventTransformer transformer; - - setUp(() { - transformer = Bloc.transformer; - }); - - tearDown(() { - Bloc.transformer = transformer; - }); - test('processes events concurrently by default', () async { final states = []; final bloc = CounterBloc() @@ -196,77 +186,81 @@ void main() { test( 'processes events sequentially when ' 'Bloc.transformer is overridden.', () async { - Bloc.transformer = (events, mapper) => events.asyncExpand(mapper); - final states = []; - final bloc = CounterBloc() - ..stream.listen(states.add) - ..add(Increment()) - ..add(Increment()) - ..add(Increment()); + await BlocOverrides.runZoned( + () async { + final states = []; + final bloc = CounterBloc() + ..stream.listen(states.add) + ..add(Increment()) + ..add(Increment()) + ..add(Increment()); - await tick(); + await tick(); - expect( - bloc.onCalls, - equals([Increment()]), - ); + expect( + bloc.onCalls, + equals([Increment()]), + ); - await wait(); + await wait(); - expect( - bloc.onEmitCalls, - equals([Increment()]), - ); - expect(states, equals([1])); + expect( + bloc.onEmitCalls, + equals([Increment()]), + ); + expect(states, equals([1])); - await tick(); + await tick(); - expect( - bloc.onCalls, - equals([Increment(), Increment()]), - ); + expect( + bloc.onCalls, + equals([Increment(), Increment()]), + ); - await wait(); + await wait(); - expect( - bloc.onEmitCalls, - equals([Increment(), Increment()]), - ); + expect( + bloc.onEmitCalls, + equals([Increment(), Increment()]), + ); - expect(states, equals([1, 2])); + expect(states, equals([1, 2])); - await tick(); + await tick(); - expect( - bloc.onCalls, - equals([ - Increment(), - Increment(), - Increment(), - ]), - ); + expect( + bloc.onCalls, + equals([ + Increment(), + Increment(), + Increment(), + ]), + ); - await wait(); + await wait(); - expect( - bloc.onEmitCalls, - equals([Increment(), Increment(), Increment()]), - ); + expect( + bloc.onEmitCalls, + equals([Increment(), Increment(), Increment()]), + ); - expect(states, equals([1, 2, 3])); + expect(states, equals([1, 2, 3])); - await bloc.close(); + await bloc.close(); - expect( - bloc.onCalls, - equals([Increment(), Increment(), Increment()]), - ); + expect( + bloc.onCalls, + equals([Increment(), Increment(), Increment()]), + ); - expect( - bloc.onEmitCalls, - equals([Increment(), Increment(), Increment()]), - ); + expect( + bloc.onEmitCalls, + equals([Increment(), Increment(), Increment()]), + ); - expect(states, equals([1, 2, 3])); + expect(states, equals([1, 2, 3])); + }, + eventTransformer: (events, mapper) => events.asyncExpand(mapper), + ); }); } diff --git a/packages/bloc/test/bloc_observer_test.dart b/packages/bloc/test/bloc_observer_test.dart index 4ccf8dd6ae0..70f644154d9 100644 --- a/packages/bloc/test/bloc_observer_test.dart +++ b/packages/bloc/test/bloc_observer_test.dart @@ -3,6 +3,8 @@ import 'package:test/test.dart'; import 'blocs/blocs.dart'; +class DefaultBlocObserver extends BlocObserver {} + void main() { final bloc = CounterBloc(); final error = Exception(); @@ -18,42 +20,42 @@ void main() { group('onCreate', () { test('does nothing by default', () { // ignore: invalid_use_of_protected_member - BlocObserver().onCreate(bloc); + DefaultBlocObserver().onCreate(bloc); }); }); group('onEvent', () { test('does nothing by default', () { // ignore: invalid_use_of_protected_member - BlocObserver().onEvent(bloc, event); + DefaultBlocObserver().onEvent(bloc, event); }); }); group('onChange', () { test('does nothing by default', () { // ignore: invalid_use_of_protected_member - BlocObserver().onChange(bloc, change); + DefaultBlocObserver().onChange(bloc, change); }); }); group('onTransition', () { test('does nothing by default', () { // ignore: invalid_use_of_protected_member - BlocObserver().onTransition(bloc, transition); + DefaultBlocObserver().onTransition(bloc, transition); }); }); group('onError', () { test('does nothing by default', () { // ignore: invalid_use_of_protected_member - BlocObserver().onError(bloc, error, stackTrace); + DefaultBlocObserver().onError(bloc, error, stackTrace); }); }); group('onClose', () { test('does nothing by default', () { // ignore: invalid_use_of_protected_member - BlocObserver().onClose(bloc); + DefaultBlocObserver().onClose(bloc); }); }); }); diff --git a/packages/bloc/test/bloc_on_test.dart b/packages/bloc/test/bloc_on_test.dart index 6652b5044e5..51e8e7169cd 100644 --- a/packages/bloc/test/bloc_on_test.dart +++ b/packages/bloc/test/bloc_on_test.dart @@ -53,7 +53,7 @@ class MissingHandlerBloc extends Bloc { } void main() { - group('on', () { + group('on', () { test('throws StateError when handler is registered more than once', () { const expectedMessage = 'on was called multiple times. ' 'There should only be a single event handler per event type.'; @@ -62,6 +62,16 @@ void main() { expect(() => DuplicateHandlerBloc(), expected); }); + test('throws StateError when handler is missing', () { + const expectedMessage = + '''add(TestEventA) was called without a registered event handler.\n''' + '''Make sure to register a handler via on((event, emit) {...})'''; + final expected = throwsA( + isA().having((e) => e.message, 'message', expectedMessage), + ); + expect(() => MissingHandlerBloc().add(TestEventA()), expected); + }); + test('invokes all on when event E is added where E is T', () async { var onEventCallCount = 0; var onACallCount = 0; diff --git a/packages/bloc/test/bloc_overrides_test.dart b/packages/bloc/test/bloc_overrides_test.dart new file mode 100644 index 00000000000..dfce6fa3dc0 --- /dev/null +++ b/packages/bloc/test/bloc_overrides_test.dart @@ -0,0 +1,116 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc/src/bloc.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class FakeBlocObserver extends Fake implements BlocObserver {} + +void main() { + group('BlocOverrides', () { + group('runZoned', () { + test('uses default BlocObserver when not specified', () { + BlocOverrides.runZoned(() { + final overrides = BlocOverrides.current; + expect(overrides!.blocObserver, isA()); + }); + }); + + test('uses default EventTransformer when not specified', () { + BlocOverrides.runZoned(() { + final overrides = BlocOverrides.current; + expect(overrides!.eventTransformer, isA()); + }); + }); + + test('uses custom BlocObserver when specified', () { + final blocObserver = FakeBlocObserver(); + BlocOverrides.runZoned(() { + final overrides = BlocOverrides.current; + expect(overrides!.blocObserver, equals(blocObserver)); + }, blocObserver: blocObserver); + }); + + test('uses custom EventTransformer when specified', () { + final eventTransformer = (Stream events, EventMapper mapper) { + return events.asyncExpand(mapper); + }; + BlocOverrides.runZoned(() { + final overrides = BlocOverrides.current; + expect(overrides!.eventTransformer, equals(eventTransformer)); + }, eventTransformer: eventTransformer); + }); + + test( + 'uses current BlocObserver when not specified ' + 'and zone already contains a BlocObserver', () { + final blocObserver = FakeBlocObserver(); + BlocOverrides.runZoned(() { + BlocOverrides.runZoned(() { + final overrides = BlocOverrides.current; + expect(overrides!.blocObserver, equals(blocObserver)); + }); + }, blocObserver: blocObserver); + }); + + test( + 'uses current EventTransformer when not specified ' + 'and zone already contains an EventTransformer', () { + final eventTransformer = (Stream events, EventMapper mapper) { + return events.asyncExpand(mapper); + }; + BlocOverrides.runZoned(() { + BlocOverrides.runZoned(() { + final overrides = BlocOverrides.current; + expect(overrides!.eventTransformer, equals(eventTransformer)); + }); + }, eventTransformer: eventTransformer); + }); + + test( + 'uses nested BlocObserver when specified ' + 'and zone already contains a BlocObserver', () { + final rootBlocObserver = FakeBlocObserver(); + BlocOverrides.runZoned(() { + final nestedBlocObserver = FakeBlocObserver(); + final overrides = BlocOverrides.current; + expect(overrides!.blocObserver, equals(rootBlocObserver)); + BlocOverrides.runZoned(() { + final overrides = BlocOverrides.current; + expect(overrides!.blocObserver, equals(nestedBlocObserver)); + }, blocObserver: nestedBlocObserver); + }, blocObserver: rootBlocObserver); + }); + + test( + 'uses nested EventTransformer when specified ' + 'and zone already contains an EventTransformer', () { + final rootEventTransformer = (Stream events, EventMapper mapper) { + return events.asyncExpand(mapper); + }; + BlocOverrides.runZoned(() { + final nestedEventTransformer = (Stream events, EventMapper mapper) { + return events.asyncExpand(mapper); + }; + final overrides = BlocOverrides.current; + expect(overrides!.eventTransformer, equals(rootEventTransformer)); + BlocOverrides.runZoned(() { + final overrides = BlocOverrides.current; + expect(overrides!.eventTransformer, equals(nestedEventTransformer)); + }, eventTransformer: nestedEventTransformer); + }, eventTransformer: rootEventTransformer); + }); + + test('overrides cannot be mutated after zone is created', () { + final originalBlocObserver = FakeBlocObserver(); + final otherBlocObserver = FakeBlocObserver(); + var blocObserver = originalBlocObserver; + BlocOverrides.runZoned(() { + blocObserver = otherBlocObserver; + final overrides = BlocOverrides.current!; + expect(overrides.blocObserver, equals(originalBlocObserver)); + expect(overrides.blocObserver, isNot(equals(otherBlocObserver))); + }, blocObserver: blocObserver); + }); + }); + }); +} diff --git a/packages/bloc/test/bloc_test.dart b/packages/bloc/test/bloc_test.dart index de6b4423a85..df0d7dabaab 100644 --- a/packages/bloc/test/bloc_test.dart +++ b/packages/bloc/test/bloc_test.dart @@ -21,20 +21,23 @@ void main() { setUp(() { simpleBloc = SimpleBloc(); observer = MockBlocObserver(); - Bloc.observer = observer; }); test('triggers onCreate on observer when instantiated', () { - final bloc = SimpleBloc(); - // ignore: invalid_use_of_protected_member - verify(() => observer.onCreate(bloc)).called(1); + BlocOverrides.runZoned(() { + final bloc = SimpleBloc(); + // ignore: invalid_use_of_protected_member + verify(() => observer.onCreate(bloc)).called(1); + }, blocObserver: observer); }); 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); + await BlocOverrides.runZoned(() async { + final bloc = SimpleBloc(); + await bloc.close(); + // ignore: invalid_use_of_protected_member + verify(() => observer.onClose(bloc)).called(1); + }, blocObserver: observer); }); test('close does not emit new states over the state stream', () async { @@ -50,71 +53,77 @@ void main() { }); test('should map single event to correct state', () { - final expectedStates = ['data', emitsDone]; + BlocOverrides.runZoned(() { + final expectedStates = ['data', emitsDone]; + final simpleBloc = SimpleBloc(); - expectLater( - simpleBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - simpleBloc, - const Transition( - currentState: '', - event: 'event', - nextState: 'data', + expectLater( + simpleBloc.stream, + emitsInOrder(expectedStates), + ).then((dynamic _) { + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + simpleBloc, + const Transition( + currentState: '', + event: 'event', + nextState: 'data', + ), ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - simpleBloc, - const Change(currentState: '', nextState: 'data'), - ), - ).called(1); - expect(simpleBloc.state, 'data'); - }); + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + simpleBloc, + const Change(currentState: '', nextState: 'data'), + ), + ).called(1); + expect(simpleBloc.state, 'data'); + }); - simpleBloc - ..add('event') - ..close(); + simpleBloc + ..add('event') + ..close(); + }, blocObserver: observer); }); test('should map multiple events to correct states', () { - final expectedStates = ['data', emitsDone]; + BlocOverrides.runZoned(() { + final expectedStates = ['data', emitsDone]; + final simpleBloc = SimpleBloc(); - expectLater( - simpleBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - simpleBloc, - const Transition( - currentState: '', - event: 'event1', - nextState: 'data', + expectLater( + simpleBloc.stream, + emitsInOrder(expectedStates), + ).then((dynamic _) { + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + simpleBloc, + const Transition( + currentState: '', + event: 'event1', + nextState: 'data', + ), ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - simpleBloc, - const Change(currentState: '', nextState: 'data'), - ), - ).called(1); - expect(simpleBloc.state, 'data'); - }); + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + simpleBloc, + const Change(currentState: '', nextState: 'data'), + ), + ).called(1); + expect(simpleBloc.state, 'data'); + }); - simpleBloc - ..add('event1') - ..add('event2') - ..add('event3') - ..close(); + simpleBloc + ..add('event1') + ..add('event2') + ..add('event3') + ..close(); + }, blocObserver: observer); }); test('is a broadcast stream', () { @@ -147,7 +156,6 @@ void main() { setUp(() { complexBloc = ComplexBloc(); observer = MockBlocObserver(); - Bloc.observer = observer; }); test('close does not emit new states over the state stream', () async { @@ -165,37 +173,40 @@ void main() { }); test('should map single event to correct state', () { - final expectedStates = [ComplexStateB()]; + BlocOverrides.runZoned(() { + final expectedStates = [ComplexStateB()]; + final complexBloc = ComplexBloc(); - expectLater( - complexBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - complexBloc, - Transition( - currentState: ComplexStateA(), - event: ComplexEventB(), - nextState: ComplexStateB(), + expectLater( + complexBloc.stream, + emitsInOrder(expectedStates), + ).then((dynamic _) { + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + complexBloc, + Transition( + currentState: ComplexStateA(), + event: ComplexEventB(), + nextState: ComplexStateB(), + ), ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - complexBloc, - Change( - currentState: ComplexStateA(), - nextState: ComplexStateB(), + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + complexBloc, + Change( + currentState: ComplexStateA(), + nextState: ComplexStateB(), + ), ), - ), - ).called(1); - expect(complexBloc.state, ComplexStateB()); - }); + ).called(1); + expect(complexBloc.state, ComplexStateB()); + }); - complexBloc.add(ComplexEventB()); + complexBloc.add(ComplexEventB()); + }, blocObserver: observer); }); test('should map multiple events to correct states', () async { @@ -262,7 +273,6 @@ void main() { }, ); observer = MockBlocObserver(); - Bloc.observer = observer; }); test('state returns correct value initially', () { @@ -272,116 +282,132 @@ void main() { }); test('single Increment event updates state to 1', () { - final expectedStates = [1, emitsDone]; - final expectedTransitions = [ - '''Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }''' - ]; + BlocOverrides.runZoned(() { + final expectedStates = [1, emitsDone]; + final expectedTransitions = [ + '''Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }''' + ]; + final counterBloc = CounterBloc( + onEventCallback: events.add, + onTransitionCallback: (transition) { + transitions.add(transition.toString()); + }, + ); - expectLater( - counterBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - expectLater(transitions, expectedTransitions); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - counterBloc, - const Transition( - currentState: 0, - event: CounterEvent.increment, - nextState: 1, + expectLater( + counterBloc.stream, + emitsInOrder(expectedStates), + ).then((dynamic _) { + expectLater(transitions, expectedTransitions); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + counterBloc, + const Transition( + currentState: 0, + event: CounterEvent.increment, + nextState: 1, + ), ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - counterBloc, - const Change(currentState: 0, nextState: 1), - ), - ).called(1); - expect(counterBloc.state, 1); - }); + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + counterBloc, + const Change(currentState: 0, nextState: 1), + ), + ).called(1); + expect(counterBloc.state, 1); + }); - counterBloc - ..add(CounterEvent.increment) - ..close(); + counterBloc + ..add(CounterEvent.increment) + ..close(); + }, blocObserver: observer); }); test('multiple Increment event updates state to 3', () { - final expectedStates = [1, 2, 3, emitsDone]; - final expectedTransitions = [ - '''Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }''', - '''Transition { currentState: 1, event: CounterEvent.increment, nextState: 2 }''', - '''Transition { currentState: 2, event: CounterEvent.increment, nextState: 3 }''', - ]; + BlocOverrides.runZoned(() { + final expectedStates = [1, 2, 3, emitsDone]; + final expectedTransitions = [ + '''Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }''', + '''Transition { currentState: 1, event: CounterEvent.increment, nextState: 2 }''', + '''Transition { currentState: 2, event: CounterEvent.increment, nextState: 3 }''', + ]; + final counterBloc = CounterBloc( + onEventCallback: events.add, + onTransitionCallback: (transition) { + transitions.add(transition.toString()); + }, + ); - expectLater( - counterBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - expect(transitions, expectedTransitions); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - counterBloc, - const Transition( - currentState: 0, - event: CounterEvent.increment, - nextState: 1, + expectLater( + counterBloc.stream, + emitsInOrder(expectedStates), + ).then((dynamic _) { + expect(transitions, expectedTransitions); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + counterBloc, + const Transition( + currentState: 0, + event: CounterEvent.increment, + nextState: 1, + ), ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - counterBloc, - const Change(currentState: 0, nextState: 1), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - counterBloc, - const Transition( - currentState: 1, - event: CounterEvent.increment, - nextState: 2, + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + counterBloc, + const Change(currentState: 0, nextState: 1), ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - counterBloc, - const Change(currentState: 1, nextState: 2), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - counterBloc, - const Transition( - currentState: 2, - event: CounterEvent.increment, - nextState: 3, + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + counterBloc, + const Transition( + currentState: 1, + event: CounterEvent.increment, + nextState: 2, + ), ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - counterBloc, - const Change(currentState: 2, nextState: 3), - ), - ).called(1); - }); + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + counterBloc, + const Change(currentState: 1, nextState: 2), + ), + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + counterBloc, + const Transition( + currentState: 2, + event: CounterEvent.increment, + nextState: 3, + ), + ), + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + counterBloc, + const Change(currentState: 2, nextState: 3), + ), + ).called(1); + }); - counterBloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment) - ..add(CounterEvent.increment) - ..close(); + counterBloc + ..add(CounterEvent.increment) + ..add(CounterEvent.increment) + ..add(CounterEvent.increment) + ..close(); + }, blocObserver: observer); }); test('is a broadcast stream', () { @@ -465,14 +491,13 @@ void main() { late MockBlocObserver observer; setUpAll(() { - registerFallbackValue>(FakeBlocBase()); - registerFallbackValue(StackTrace.empty); + registerFallbackValue(FakeBlocBase()); + registerFallbackValue(StackTrace.empty); }); setUp(() { asyncBloc = AsyncBloc(); observer = MockBlocObserver(); - Bloc.observer = observer; }); test('close does not emit new states over the state stream', () async { @@ -486,21 +511,22 @@ void main() { test( 'close while events are pending finishes processing pending events ' 'and does not trigger onError', () async { - final expectedStates = [ - AsyncState.initial().copyWith(isLoading: true), - AsyncState.initial().copyWith(isSuccess: true), - ]; - final states = []; - - asyncBloc - ..stream.listen(states.add) - ..add(AsyncEvent()); + await BlocOverrides.runZoned(() async { + final expectedStates = [ + AsyncState.initial().copyWith(isLoading: true), + AsyncState.initial().copyWith(isSuccess: true), + ]; + final states = []; + final asyncBloc = AsyncBloc() + ..stream.listen(states.add) + ..add(AsyncEvent()); - await asyncBloc.close(); + await asyncBloc.close(); - expect(states, expectedStates); - // ignore: invalid_use_of_protected_member - verifyNever(() => observer.onError(any(), any(), any())); + expect(states, expectedStates); + // ignore: invalid_use_of_protected_member + verifyNever(() => observer.onError(any(), any(), any())); + }, blocObserver: observer); }); test('state returns correct value initially', () { @@ -508,206 +534,212 @@ void main() { }); test('should map single event to correct state', () { - final expectedStates = [ - AsyncState(isLoading: true, hasError: false, isSuccess: false), - AsyncState(isLoading: false, hasError: false, isSuccess: true), - emitsDone, - ]; + BlocOverrides.runZoned(() { + final expectedStates = [ + AsyncState(isLoading: true, hasError: false, isSuccess: false), + AsyncState(isLoading: false, hasError: false, isSuccess: true), + emitsDone, + ]; + final asyncBloc = AsyncBloc(); - expectLater( - asyncBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - asyncBloc, - Transition( - currentState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: false, - ), - event: AsyncEvent(), - nextState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, + expectLater( + asyncBloc.stream, + emitsInOrder(expectedStates), + ).then((dynamic _) { + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + asyncBloc, + Transition( + currentState: AsyncState( + isLoading: false, + hasError: false, + isSuccess: false, + ), + event: AsyncEvent(), + nextState: AsyncState( + isLoading: true, + hasError: false, + isSuccess: false, + ), ), ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - asyncBloc, - Change( - currentState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: false, - ), - nextState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + asyncBloc, + Change( + currentState: AsyncState( + isLoading: false, + 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( - currentState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, - ), - event: AsyncEvent(), - nextState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: true, + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + asyncBloc, + Transition( + currentState: AsyncState( + isLoading: true, + hasError: false, + isSuccess: false, + ), + event: AsyncEvent(), + nextState: AsyncState( + isLoading: false, + hasError: false, + isSuccess: true, + ), ), ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - asyncBloc, - Change( - currentState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, - ), - nextState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: true, + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + asyncBloc, + Change( + currentState: AsyncState( + isLoading: true, + hasError: false, + isSuccess: false, + ), + nextState: AsyncState( + isLoading: false, + hasError: false, + isSuccess: true, + ), ), ), - ), - ).called(1); - expect( - asyncBloc.state, - AsyncState( - isLoading: false, - hasError: false, - isSuccess: true, - ), - ); - }); + ).called(1); + expect( + asyncBloc.state, + AsyncState( + isLoading: false, + hasError: false, + isSuccess: true, + ), + ); + }); - asyncBloc - ..add(AsyncEvent()) - ..close(); + asyncBloc + ..add(AsyncEvent()) + ..close(); + }, blocObserver: observer); }); test('should map multiple events to correct states', () { - final expectedStates = [ - AsyncState(isLoading: true, hasError: false, isSuccess: false), - AsyncState(isLoading: false, hasError: false, isSuccess: true), - AsyncState(isLoading: true, hasError: false, isSuccess: false), - AsyncState(isLoading: false, hasError: false, isSuccess: true), - emitsDone, - ]; + BlocOverrides.runZoned(() { + final expectedStates = [ + AsyncState(isLoading: true, hasError: false, isSuccess: false), + AsyncState(isLoading: false, hasError: false, isSuccess: true), + AsyncState(isLoading: true, hasError: false, isSuccess: false), + AsyncState(isLoading: false, hasError: false, isSuccess: true), + emitsDone, + ]; + final asyncBloc = AsyncBloc(); - expectLater( - asyncBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - asyncBloc, - Transition( - currentState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: false, - ), - event: AsyncEvent(), - nextState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, + expectLater( + asyncBloc.stream, + emitsInOrder(expectedStates), + ).then((dynamic _) { + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + asyncBloc, + Transition( + currentState: AsyncState( + isLoading: false, + hasError: false, + isSuccess: false, + ), + event: AsyncEvent(), + nextState: AsyncState( + isLoading: true, + hasError: false, + isSuccess: false, + ), ), ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - asyncBloc, - Change( - currentState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: false, - ), - nextState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + asyncBloc, + Change( + currentState: AsyncState( + isLoading: false, + 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( - currentState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, - ), - event: AsyncEvent(), - nextState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: true, + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onTransition( + asyncBloc, + Transition( + currentState: AsyncState( + isLoading: true, + hasError: false, + isSuccess: false, + ), + event: AsyncEvent(), + nextState: AsyncState( + isLoading: false, + hasError: false, + isSuccess: true, + ), ), ), - ), - ).called(2); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - asyncBloc, - Change( - currentState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, - ), - nextState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: true, + ).called(2); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + asyncBloc, + Change( + currentState: AsyncState( + isLoading: true, + hasError: false, + isSuccess: false, + ), + nextState: AsyncState( + isLoading: false, + hasError: false, + isSuccess: true, + ), ), ), - ), - ).called(2); - expect( - asyncBloc.state, - AsyncState( - isLoading: false, - hasError: false, - isSuccess: true, - ), - ); - }); + ).called(2); + expect( + asyncBloc.state, + AsyncState( + isLoading: false, + hasError: false, + isSuccess: true, + ), + ); + }); - asyncBloc - ..add(AsyncEvent()) - ..add(AsyncEvent()) - ..close(); + asyncBloc + ..add(AsyncEvent()) + ..add(AsyncEvent()) + ..close(); + }, blocObserver: observer); }); test('is a broadcast stream', () { @@ -1058,11 +1090,7 @@ void main() { await bloc.close(); }, (error, stackTrace) => uncaughtError = error); expect(states, equals(expectedStates)); - expect( - uncaughtError, - isA() - .having((e) => e.error, 'error', error), - ); + expect(uncaughtError, equals(error)); }); test('unawaited onEach throws AssertionError', () async { @@ -1269,11 +1297,7 @@ void main() { }, (error, stack) => uncaughtError = error); expect(states, equals(expectedStates)); - expect( - uncaughtError, - isA() - .having((e) => e.error, 'error', error), - ); + expect(uncaughtError, equals(error)); }); }); @@ -1321,13 +1345,7 @@ void main() { ..add(CounterEvent.decrement) ..close(); }, (Object error, StackTrace stackTrace) { - expect( - (error as BlocUnhandledErrorException).toString(), - contains( - 'Unhandled error Exception: fatal exception occurred ' - 'in Instance of \'CounterExceptionBloc\'.', - ), - ); + expect(error.toString(), equals('Exception: fatal exception')); expect(stackTrace, isNotNull); expect(stackTrace, isNot(StackTrace.empty)); }); @@ -1342,21 +1360,15 @@ void main() { onErrorCallback: (Object _, StackTrace __) {}, )..addError(expectedError, StackTrace.current); }, (Object error, StackTrace stackTrace) { - expect( - (error as BlocUnhandledErrorException).toString(), - contains( - 'Unhandled error Exception: fatal exception occurred ' - 'in Instance of \'OnExceptionBloc\'.', - ), - ); + expect(error, equals(expectedError)); expect(stackTrace, isNotNull); expect(stackTrace, isNot(StackTrace.empty)); }); }); test('triggers onError from on', () { + final exception = Exception('fatal exception'); runZonedGuarded(() { - final exception = Exception('fatal exception'); Object? expectedError; StackTrace? expectedStacktrace; @@ -1381,42 +1393,32 @@ void main() { ..add(CounterEvent.increment) ..close(); }, (Object error, StackTrace stackTrace) { - expect( - (error as BlocUnhandledErrorException).toString(), - contains( - 'Unhandled error Exception: fatal exception occurred ' - 'in Instance of \'OnExceptionBloc\'.', - ), - ); + expect(error, equals(exception)); expect(stackTrace, isNotNull); expect(stackTrace, isNot(StackTrace.empty)); }); }); test('triggers onError from onEvent', () { + final exception = Exception('fatal exception'); runZonedGuarded(() { - final exception = Exception('fatal exception'); - OnEventErrorBloc(exception: exception) ..add(CounterEvent.increment) ..close(); }, (Object error, StackTrace stackTrace) { - expect( - (error as BlocUnhandledErrorException).toString(), - contains( - 'Unhandled error Exception: fatal exception occurred ' - 'in Instance of \'OnEventErrorBloc\'.', - ), - ); + expect(error, equals(exception)); expect(stackTrace, isNotNull); expect(stackTrace, isNot(StackTrace.empty)); }); }); - test('does not trigger onError from add', () { + test( + 'add throws StateError and triggers onError ' + 'when bloc is closed', () { + Object? capturedError; + StackTrace? capturedStacktrace; + var didThrow = false; runZonedGuarded(() { - Object? capturedError; - StackTrace? capturedStacktrace; final counterBloc = CounterBloc( onErrorCallback: (error, stackTrace) { capturedError = error; @@ -1427,17 +1429,27 @@ void main() { expectLater( counterBloc.stream, emitsInOrder([emitsDone]), - ).then((dynamic _) { - expect(capturedError, isNull); - expect(capturedStacktrace, isNull); - }); + ); counterBloc ..close() ..add(CounterEvent.increment); - }, (Object _, StackTrace __) { - fail('should not throw when add is called after bloc is closed'); + }, (Object error, StackTrace stackTrace) { + didThrow = true; + expect(error, equals(capturedError)); + expect(stackTrace, equals(capturedStacktrace)); }); + + expect(didThrow, isTrue); + expect( + capturedError, + isA().having( + (e) => e.message, + 'message', + 'Cannot add new events after calling close', + ), + ); + expect(capturedStacktrace, isNotNull); }); }); @@ -1533,6 +1545,38 @@ void main() { expect(counterBloc.state, 42); await counterBloc.close(); }); + + test( + 'throws StateError and triggers onError ' + 'when bloc is closed', () async { + Object? capturedError; + StackTrace? capturedStacktrace; + + final states = []; + final expectedStateError = isA().having( + (e) => e.message, + 'message', + 'Cannot emit new states after calling close', + ); + + final counterBloc = CounterBloc( + onErrorCallback: (error, stackTrace) { + capturedError = error; + capturedStacktrace = stackTrace; + }, + )..stream.listen(states.add); + + await counterBloc.close(); + + expect(counterBloc.isClosed, isTrue); + expect(counterBloc.state, equals(0)); + expect(states, isEmpty); + expect(() => counterBloc.emit(1), throwsA(expectedStateError)); + expect(counterBloc.state, equals(0)); + expect(states, isEmpty); + expect(capturedError, expectedStateError); + expect(capturedStacktrace, isNotNull); + }); }); group('close', () { diff --git a/packages/bloc/test/cubit_test.dart b/packages/bloc/test/cubit_test.dart index 3f657205ac7..2a5a21fdc07 100644 --- a/packages/bloc/test/cubit_test.dart +++ b/packages/bloc/test/cubit_test.dart @@ -19,13 +19,14 @@ void main() { setUp(() { observer = MockBlocObserver(); - Bloc.observer = observer; }); test('triggers onCreate on observer', () { - final cubit = CounterCubit(); - // ignore: invalid_use_of_protected_member - verify(() => observer.onCreate(cubit)).called(1); + BlocOverrides.runZoned(() { + final cubit = CounterCubit(); + // ignore: invalid_use_of_protected_member + verify(() => observer.onCreate(cubit)).called(1); + }, blocObserver: observer); }); }); @@ -36,29 +37,35 @@ void main() { }); group('addError', () { - BlocObserver observer; + late BlocObserver observer; setUp(() { observer = MockBlocObserver(); - Bloc.observer = observer; }); test('triggers onError', () async { - final expectedError = Exception('fatal exception'); - - runZonedGuarded(() { - CounterCubit().addError(expectedError, StackTrace.current); - }, (Object error, StackTrace stackTrace) { - expect( - (error as BlocUnhandledErrorException).toString(), - contains( - 'Unhandled error Exception: fatal exception occurred ' - 'in Instance of \'CounterCubit\'.', - ), - ); - expect(stackTrace, isNotNull); - expect(stackTrace, isNot(StackTrace.empty)); - }); + BlocOverrides.runZoned(() { + final expectedError = Exception('fatal exception'); + final expectedStackTrace = StackTrace.current; + final errors = []; + final stackTraces = []; + final cubit = CounterCubit( + onErrorCallback: (error, stackTrace) { + errors.add(error); + stackTraces.add(stackTrace); + }, + )..addError(expectedError, expectedStackTrace); + + expect(errors.length, equals(1)); + expect(errors.first, equals(expectedError)); + expect(stackTraces.length, equals(1)); + expect(stackTraces.first, isNotNull); + expect(stackTraces.first, isNot(StackTrace.empty)); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onError(cubit, expectedError, expectedStackTrace), + ).called(1); + }, blocObserver: observer); }); }); @@ -66,102 +73,103 @@ void main() { late BlocObserver observer; setUpAll(() { - registerFallbackValue>(FakeBlocBase()); - registerFallbackValue>(FakeChange()); + registerFallbackValue(FakeBlocBase()); + registerFallbackValue(FakeChange()); }); setUp(() { observer = MockBlocObserver(); - Bloc.observer = observer; }); test('is not called for the initial state', () async { - final changes = >[]; - final cubit = CounterCubit(onChangeCallback: changes.add); - await cubit.close(); - expect(changes, isEmpty); - // ignore: invalid_use_of_protected_member - verifyNever(() => observer.onChange(any(), any())); + await BlocOverrides.runZoned(() async { + final changes = >[]; + final cubit = CounterCubit(onChangeCallback: changes.add); + await cubit.close(); + expect(changes, isEmpty); + // ignore: invalid_use_of_protected_member + verifyNever(() => observer.onChange(any(), any())); + }, blocObserver: observer); }); test('is called with correct change for a single state change', () async { - final changes = >[]; - final cubit = CounterCubit(onChangeCallback: changes.add)..increment(); - await cubit.close(); - expect( - changes, - const [Change(currentState: 0, nextState: 1)], - ); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - cubit, - const Change(currentState: 0, nextState: 1), - ), - ).called(1); + await BlocOverrides.runZoned(() async { + final changes = >[]; + final cubit = CounterCubit(onChangeCallback: changes.add) + ..increment(); + await cubit.close(); + expect( + changes, + const [Change(currentState: 0, nextState: 1)], + ); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + cubit, + const Change(currentState: 0, nextState: 1), + ), + ).called(1); + }, blocObserver: observer); }); test('is called with correct changes for multiple state changes', () async { - final changes = >[]; - final cubit = CounterCubit(onChangeCallback: changes.add) - ..increment() - ..increment(); - await cubit.close(); - expect( - changes, - const [ - Change(currentState: 0, nextState: 1), - Change(currentState: 1, nextState: 2), - ], - ); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - cubit, - const Change(currentState: 0, nextState: 1), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - cubit, - const Change(currentState: 1, nextState: 2), - ), - ).called(1); + await BlocOverrides.runZoned(() async { + final changes = >[]; + final cubit = CounterCubit(onChangeCallback: changes.add) + ..increment() + ..increment(); + await cubit.close(); + expect( + changes, + const [ + Change(currentState: 0, nextState: 1), + Change(currentState: 1, nextState: 2), + ], + ); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + cubit, + const Change(currentState: 0, nextState: 1), + ), + ).called(1); + verify( + // ignore: invalid_use_of_protected_member + () => observer.onChange( + cubit, + const Change(currentState: 1, nextState: 2), + ), + ).called(1); + }, blocObserver: observer); }); }); group('emit', () { - test('does nothing if cubit is closed (indirect)', () { - final cubit = CounterCubit(); - expectLater( - cubit.stream, - emitsInOrder([equals(1), emitsDone]), - ); - cubit - ..increment() - ..close() - ..increment(); - }); - - test('does nothing if cubit is closed (direct)', () { - final cubit = CounterCubit(); - expectLater( - cubit.stream, - emitsInOrder([equals(1), emitsDone]), - ); - cubit - ..emit(1) - ..close() - ..emit(2); - }); - - test('can be invoked directly within a test', () { - final cubit = CounterCubit() - ..emit(100) - ..close(); - expect(cubit.state, 100); + test('throws StateError if cubit is closed', () { + var didThrow = false; + runZonedGuarded(() { + final cubit = CounterCubit(); + expectLater( + cubit.stream, + emitsInOrder([equals(1), emitsDone]), + ); + cubit + ..increment() + ..close() + ..increment(); + }, (error, _) { + didThrow = true; + expect( + error, + isA().having( + (e) => e.message, + 'message', + 'Cannot emit new states after calling close', + ), + ); + }); + expect(didThrow, isTrue); }); test('emits states in the correct order', () async { @@ -262,70 +270,20 @@ void main() { }); }); - group('listen (legacy)', () { - test('returns a StreamSubscription', () { - final cubit = CounterCubit(); - // ignore: deprecated_member_use_from_same_package - final subscription = cubit.listen((_) {}); - expect(subscription, isA>()); - subscription.cancel(); - cubit.close(); - }); - - test('does not receive current state upon subscribing', () async { - final states = []; - // ignore: deprecated_member_use_from_same_package - final cubit = CounterCubit()..listen(states.add); - await cubit.close(); - expect(states, isEmpty); - }); - - test('receives single async state', () async { - final states = []; - // ignore: deprecated_member_use_from_same_package - final cubit = FakeAsyncCounterCubit()..listen(states.add); - await cubit.increment(); - await cubit.close(); - expect(states, [equals(1)]); - }); - - test('receives multiple async states', () async { - final states = []; - // ignore: deprecated_member_use_from_same_package - final cubit = FakeAsyncCounterCubit()..listen(states.add); - await cubit.increment(); - await cubit.increment(); - await cubit.increment(); - await cubit.close(); - expect(states, [equals(1), equals(2), equals(3)]); - }); - - test('can call listen multiple times', () async { - final states = []; - final cubit = CounterCubit() - // ignore: deprecated_member_use_from_same_package - ..listen(states.add) - // ignore: deprecated_member_use_from_same_package - ..listen(states.add) - ..increment(); - await cubit.close(); - expect(states, [equals(1), equals(1)]); - }); - }); - group('close', () { late MockBlocObserver observer; setUp(() { observer = MockBlocObserver(); - Bloc.observer = observer; }); test('triggers onClose on observer', () async { - final cubit = CounterCubit(); - await cubit.close(); - // ignore: invalid_use_of_protected_member - verify(() => observer.onClose(cubit)).called(1); + await BlocOverrides.runZoned(() async { + final cubit = CounterCubit(); + await cubit.close(); + // ignore: invalid_use_of_protected_member + verify(() => observer.onClose(cubit)).called(1); + }, blocObserver: observer); }); test('emits done (sync)', () { diff --git a/packages/bloc/test/cubits/counter_cubit.dart b/packages/bloc/test/cubits/counter_cubit.dart index 27661acc163..19259beae1d 100644 --- a/packages/bloc/test/cubits/counter_cubit.dart +++ b/packages/bloc/test/cubits/counter_cubit.dart @@ -1,9 +1,10 @@ import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { - CounterCubit({this.onChangeCallback}) : super(0); + CounterCubit({this.onChangeCallback, this.onErrorCallback}) : super(0); final void Function(Change)? onChangeCallback; + final void Function(Object error, StackTrace stackTrace)? onErrorCallback; void increment() => emit(state + 1); void decrement() => emit(state - 1); @@ -13,4 +14,10 @@ class CounterCubit extends Cubit { super.onChange(change); onChangeCallback?.call(change); } + + @override + void onError(Object error, StackTrace stackTrace) { + onErrorCallback?.call(error, stackTrace); + super.onError(error, stackTrace); + } } diff --git a/packages/bloc/test/legacy/bloc_observer_test.dart b/packages/bloc/test/legacy/bloc_observer_test.dart deleted file mode 100644 index 7e45f6c2028..00000000000 --- a/packages/bloc/test/legacy/bloc_observer_test.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:test/test.dart'; - -import 'blocs/blocs.dart'; - -void main() { - final bloc = CounterBloc(); - const event = CounterEvent.increment; - const change = Change(currentState: 0, nextState: 1); - const transition = Transition( - currentState: 0, - event: CounterEvent.increment, - nextState: 1, - ); - group('BlocObserver', () { - group('onCreate', () { - test('does nothing by default', () { - // ignore: invalid_use_of_protected_member - BlocObserver().onCreate(bloc); - }); - }); - - group('onEvent', () { - test('does nothing by default', () { - // ignore: invalid_use_of_protected_member - BlocObserver().onEvent(bloc, event); - }); - }); - - group('onChange', () { - test('does nothing by default', () { - // ignore: invalid_use_of_protected_member - BlocObserver().onChange(bloc, change); - }); - }); - - group('onTransition', () { - test('does nothing by default', () { - // ignore: invalid_use_of_protected_member - BlocObserver().onTransition(bloc, transition); - }); - }); - - group('onError', () { - test('does nothing by default', () { - // ignore: invalid_use_of_protected_member - BlocObserver().onError(bloc, event, StackTrace.current); - }); - }); - - group('onClose', () { - test('does nothing by default', () { - // ignore: invalid_use_of_protected_member - BlocObserver().onClose(bloc); - }); - }); - }); -} diff --git a/packages/bloc/test/legacy/bloc_test.dart b/packages/bloc/test/legacy/bloc_test.dart deleted file mode 100644 index 1e27c0cbd33..00000000000 --- a/packages/bloc/test/legacy/bloc_test.dart +++ /dev/null @@ -1,993 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:test/test.dart'; - -import 'blocs/blocs.dart'; - -class MockBlocObserver extends Mock implements BlocObserver {} - -class FakeBlocBase extends Fake implements BlocBase {} - -void main() { - group('Bloc Tests', () { - group('Simple Bloc', () { - late SimpleBloc simpleBloc; - late MockBlocObserver observer; - - setUp(() { - simpleBloc = SimpleBloc(); - observer = MockBlocObserver(); - Bloc.observer = observer; - }); - - 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); - }); - - test('close does not emit new states over the state stream', () async { - final expectedStates = [emitsDone]; - - unawaited(expectLater(simpleBloc.stream, emitsInOrder(expectedStates))); - - await simpleBloc.close(); - }); - - test('state returns correct value initially', () { - expect(simpleBloc.state, ''); - }); - - test('should map single event to correct state', () { - final expectedStates = ['data', emitsDone]; - - expectLater( - simpleBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - simpleBloc, - const Transition( - currentState: '', - event: 'event', - nextState: 'data', - ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - simpleBloc, - const Change(currentState: '', nextState: 'data'), - ), - ).called(1); - expect(simpleBloc.state, 'data'); - }); - - simpleBloc - ..add('event') - ..close(); - }); - - test('should map multiple events to correct states', () { - final expectedStates = ['data', emitsDone]; - - expectLater( - simpleBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - simpleBloc, - const Transition( - currentState: '', - event: 'event1', - nextState: 'data', - ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - simpleBloc, - const Change(currentState: '', nextState: 'data'), - ), - ).called(1); - expect(simpleBloc.state, 'data'); - }); - - simpleBloc - ..add('event1') - ..add('event2') - ..add('event3') - ..close(); - }); - - test('is a broadcast stream', () { - final expectedStates = ['data', emitsDone]; - - expect(simpleBloc.stream.isBroadcast, isTrue); - expectLater(simpleBloc.stream, emitsInOrder(expectedStates)); - expectLater(simpleBloc.stream, emitsInOrder(expectedStates)); - - simpleBloc - ..add('event') - ..close(); - }); - - test('multiple subscribers receive the latest state', () { - final expectedStates = const ['data']; - - expectLater(simpleBloc.stream, emitsInOrder(expectedStates)); - expectLater(simpleBloc.stream, emitsInOrder(expectedStates)); - expectLater(simpleBloc.stream, emitsInOrder(expectedStates)); - - simpleBloc.add('event'); - }); - }); - - group('Complex Bloc', () { - late ComplexBloc complexBloc; - late MockBlocObserver observer; - - setUp(() { - complexBloc = ComplexBloc(); - observer = MockBlocObserver(); - Bloc.observer = observer; - }); - - test('close does not emit new states over the state stream', () async { - final expectedStates = [emitsDone]; - - unawaited( - expectLater(complexBloc.stream, emitsInOrder(expectedStates)), - ); - - await complexBloc.close(); - }); - - test('state returns correct value initially', () { - expect(complexBloc.state, ComplexStateA()); - }); - - test('should map single event to correct state', () { - final expectedStates = [ComplexStateB()]; - - expectLater( - complexBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - complexBloc, - Transition( - currentState: ComplexStateA(), - event: ComplexEventB(), - nextState: ComplexStateB(), - ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - complexBloc, - Change( - currentState: ComplexStateA(), - nextState: ComplexStateB(), - ), - ), - ).called(1); - expect(complexBloc.state, ComplexStateB()); - }); - - complexBloc.add(ComplexEventB()); - }); - - test('should map multiple events to correct states', () async { - final expectedStates = [ - ComplexStateB(), - ComplexStateD(), - ComplexStateA(), - ComplexStateC(), - ]; - - unawaited( - expectLater(complexBloc.stream, emitsInOrder(expectedStates)), - ); - - complexBloc.add(ComplexEventA()); - await Future.delayed(const Duration(milliseconds: 20)); - complexBloc.add(ComplexEventB()); - await Future.delayed(const Duration(milliseconds: 20)); - complexBloc.add(ComplexEventC()); - await Future.delayed(const Duration(milliseconds: 20)); - complexBloc.add(ComplexEventD()); - await Future.delayed(const Duration(milliseconds: 200)); - complexBloc - ..add(ComplexEventC()) - ..add(ComplexEventA()); - await Future.delayed(const Duration(milliseconds: 120)); - complexBloc.add(ComplexEventC()); - }); - - test('is a broadcast stream', () { - final expectedStates = [ComplexStateB()]; - - expect(complexBloc.stream.isBroadcast, isTrue); - expectLater(complexBloc.stream, emitsInOrder(expectedStates)); - expectLater(complexBloc.stream, emitsInOrder(expectedStates)); - - complexBloc.add(ComplexEventB()); - }); - - test('multiple subscribers receive the latest state', () { - final expected = [ComplexStateB()]; - - expectLater(complexBloc.stream, emitsInOrder(expected)); - expectLater(complexBloc.stream, emitsInOrder(expected)); - expectLater(complexBloc.stream, emitsInOrder(expected)); - - complexBloc.add(ComplexEventB()); - }); - }); - - group('CounterBloc', () { - late CounterBloc counterBloc; - late MockBlocObserver observer; - late List transitions; - late List events; - - setUp(() { - events = []; - transitions = []; - counterBloc = CounterBloc( - onEventCallback: events.add, - onTransitionCallback: (transition) { - transitions.add(transition.toString()); - }, - ); - observer = MockBlocObserver(); - Bloc.observer = observer; - }); - - test('state returns correct value initially', () { - expect(counterBloc.state, 0); - expect(events.isEmpty, true); - expect(transitions.isEmpty, true); - }); - - test('single Increment event updates state to 1', () { - final expectedStates = [1, emitsDone]; - final expectedTransitions = [ - '''Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }''' - ]; - - expectLater( - counterBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - expectLater(transitions, expectedTransitions); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - counterBloc, - const Transition( - currentState: 0, - event: CounterEvent.increment, - nextState: 1, - ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - counterBloc, - const Change(currentState: 0, nextState: 1), - ), - ).called(1); - expect(counterBloc.state, 1); - }); - - counterBloc - ..add(CounterEvent.increment) - ..close(); - }); - - test('multiple Increment event updates state to 3', () { - final expectedStates = [1, 2, 3, emitsDone]; - final expectedTransitions = [ - '''Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }''', - '''Transition { currentState: 1, event: CounterEvent.increment, nextState: 2 }''', - '''Transition { currentState: 2, event: CounterEvent.increment, nextState: 3 }''', - ]; - - expectLater( - counterBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - expect(transitions, expectedTransitions); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - counterBloc, - const Transition( - currentState: 0, - event: CounterEvent.increment, - nextState: 1, - ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - counterBloc, - const Change(currentState: 0, nextState: 1), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - counterBloc, - const Transition( - currentState: 1, - event: CounterEvent.increment, - nextState: 2, - ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - counterBloc, - const Change(currentState: 1, nextState: 2), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - counterBloc, - const Transition( - currentState: 2, - event: CounterEvent.increment, - nextState: 3, - ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - counterBloc, - const Change(currentState: 2, nextState: 3), - ), - ).called(1); - }); - - counterBloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment) - ..add(CounterEvent.increment) - ..close(); - }); - - test('is a broadcast stream', () { - final expectedStates = [1, emitsDone]; - - expect(counterBloc.stream.isBroadcast, isTrue); - expectLater(counterBloc.stream, emitsInOrder(expectedStates)); - expectLater(counterBloc.stream, emitsInOrder(expectedStates)); - - counterBloc - ..add(CounterEvent.increment) - ..close(); - }); - - test('multiple subscribers receive the latest state', () { - const expected = [1]; - - expectLater(counterBloc.stream, emitsInOrder(expected)); - expectLater(counterBloc.stream, emitsInOrder(expected)); - expectLater(counterBloc.stream, emitsInOrder(expected)); - - counterBloc.add(CounterEvent.increment); - }); - }); - - group('Async Bloc', () { - late AsyncBloc asyncBloc; - late MockBlocObserver observer; - - setUpAll(() { - registerFallbackValue>(FakeBlocBase()); - registerFallbackValue(StackTrace.empty); - }); - - setUp(() { - asyncBloc = AsyncBloc(); - observer = MockBlocObserver(); - Bloc.observer = observer; - }); - - test('close does not emit new states over the state stream', () async { - final expectedStates = [emitsDone]; - - unawaited(expectLater(asyncBloc.stream, emitsInOrder(expectedStates))); - - await asyncBloc.close(); - }); - - test( - 'close while events are pending finishes processing pending events ' - 'and does not trigger onError', () async { - final expectedStates = [ - AsyncState.initial().copyWith(isLoading: true), - AsyncState.initial().copyWith(isSuccess: true), - ]; - final states = []; - - asyncBloc - // ignore: deprecated_member_use_from_same_package - ..listen(states.add) - ..add(AsyncEvent()); - - await asyncBloc.close(); - - expect(states, expectedStates); - // ignore: invalid_use_of_protected_member - verifyNever(() => observer.onError(any(), any(), any())); - }); - - test('state returns correct value initially', () { - expect(asyncBloc.state, AsyncState.initial()); - }); - - test('should map single event to correct state', () { - final expectedStates = [ - AsyncState(isLoading: true, hasError: false, isSuccess: false), - AsyncState(isLoading: false, hasError: false, isSuccess: true), - emitsDone, - ]; - - expectLater( - asyncBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - verify( - // ignore: invalid_use_of_protected_member - () => observer.onTransition( - asyncBloc, - Transition( - currentState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: false, - ), - event: AsyncEvent(), - nextState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, - ), - ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - asyncBloc, - Change( - currentState: AsyncState( - isLoading: false, - 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( - currentState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, - ), - event: AsyncEvent(), - nextState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: true, - ), - ), - ), - ).called(1); - verify( - // ignore: invalid_use_of_protected_member - () => observer.onChange( - asyncBloc, - Change( - currentState: AsyncState( - isLoading: true, - hasError: false, - isSuccess: false, - ), - nextState: AsyncState( - isLoading: false, - hasError: false, - isSuccess: true, - ), - ), - ), - ).called(1); - expect( - asyncBloc.state, - AsyncState( - isLoading: false, - hasError: false, - isSuccess: true, - ), - ); - }); - - asyncBloc - ..add(AsyncEvent()) - ..close(); - }); - - test('is a broadcast stream', () { - final expectedStates = [ - AsyncState(isLoading: true, hasError: false, isSuccess: false), - AsyncState(isLoading: false, hasError: false, isSuccess: true), - emitsDone, - ]; - - expect(asyncBloc.stream.isBroadcast, isTrue); - expectLater(asyncBloc.stream, emitsInOrder(expectedStates)); - expectLater(asyncBloc.stream, emitsInOrder(expectedStates)); - - asyncBloc - ..add(AsyncEvent()) - ..close(); - }); - - test('multiple subscribers receive the latest state', () { - final expected = [ - AsyncState(isLoading: true, hasError: false, isSuccess: false), - AsyncState(isLoading: false, hasError: false, isSuccess: true), - ]; - - expectLater(asyncBloc.stream, emitsInOrder(expected)); - expectLater(asyncBloc.stream, emitsInOrder(expected)); - expectLater(asyncBloc.stream, emitsInOrder(expected)); - - asyncBloc.add(AsyncEvent()); - }); - }); - - group('flatMap', () { - test('maintains correct transition composition', () { - final expectedTransitions = >[ - const Transition( - currentState: 0, - event: CounterEvent.decrement, - nextState: -1, - ), - const Transition( - currentState: -1, - event: CounterEvent.increment, - nextState: 0, - ), - ]; - - final expectedStates = [-1, 0, emitsDone]; - final transitions = >[]; - final flatMapBloc = FlatMapBloc( - onTransitionCallback: transitions.add, - ); - - expectLater( - flatMapBloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - expect(transitions, expectedTransitions); - }); - flatMapBloc - ..add(CounterEvent.decrement) - ..add(CounterEvent.increment) - ..close(); - }); - }); - - group('mergeBloc', () { - test('maintains correct transition composition', () { - final expectedTransitions = >[ - const Transition( - currentState: 0, - event: CounterEvent.increment, - nextState: 1, - ), - const Transition( - currentState: 1, - event: CounterEvent.decrement, - nextState: 0, - ), - const Transition( - currentState: 0, - event: CounterEvent.decrement, - nextState: -1, - ), - ]; - final expectedStates = [1, 0, -1, emitsDone]; - final transitions = >[]; - - final bloc = MergeBloc( - onTransitionCallback: transitions.add, - ); - - expectLater( - bloc.stream, - emitsInOrder(expectedStates), - ).then((dynamic _) { - expect(transitions, expectedTransitions); - }); - bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment) - ..add(CounterEvent.decrement) - ..add(CounterEvent.decrement) - ..close(); - }); - }); - - group('SeededBloc', () { - test('does not emit repeated states', () { - final seededBloc = SeededBloc(seed: 0, states: [1, 2, 1, 1]); - final expectedStates = [1, 2, 1, emitsDone]; - - expectLater(seededBloc.stream, emitsInOrder(expectedStates)); - - seededBloc - ..add('event') - ..close(); - }); - - test('can emit initial state only once', () { - final seededBloc = SeededBloc(seed: 0, states: [0, 0]); - final expectedStates = [0, emitsDone]; - - expectLater(seededBloc.stream, emitsInOrder(expectedStates)); - - seededBloc - ..add('event') - ..close(); - }); - - test( - 'can emit initial state and ' - 'continue emitting distinct states', () { - final seededBloc = SeededBloc(seed: 0, states: [0, 0, 1]); - final expectedStates = [0, 1, emitsDone]; - - expectLater(seededBloc.stream, emitsInOrder(expectedStates)); - - seededBloc - ..add('event') - ..close(); - }); - - test('discards subsequent duplicate states (distinct events)', () { - final seededBloc = SeededBloc(seed: 0, states: [1, 1]); - final expectedStates = [1, emitsDone]; - - expectLater(seededBloc.stream, emitsInOrder(expectedStates)); - - seededBloc - ..add('eventA') - ..add('eventB') - ..add('eventC') - ..close(); - }); - - test('discards subsequent duplicate states (same event)', () { - final seededBloc = SeededBloc(seed: 0, states: [1, 1]); - final expectedStates = [1, emitsDone]; - - expectLater(seededBloc.stream, emitsInOrder(expectedStates)); - - seededBloc - ..add('event') - ..add('event') - ..add('event') - ..close(); - }); - }); - - group('MixedCounterBloc', () { - test( - 'throws StateError when on and mapEventToState ' - 'are used in conjunction', () { - runZonedGuarded( - () => MixedCounterBloc()..add(CounterEvent.increment), - (Object error, StackTrace stackTrace) { - expect( - (error as BlocUnhandledErrorException).toString(), - contains( - 'mapEventToState cannot be overridden in ' - 'conjunction with on.', - ), - ); - }, - ); - }); - }); - - group('Exception', () { - test('does not break stream', () { - runZonedGuarded(() { - final expectedStates = [-1, emitsDone]; - final counterBloc = CounterExceptionBloc(); - - expectLater(counterBloc.stream, emitsInOrder(expectedStates)); - - counterBloc - ..add(CounterEvent.increment) - ..add(CounterEvent.decrement) - ..close(); - }, (Object error, StackTrace stackTrace) { - expect( - (error as BlocUnhandledErrorException).toString(), - contains( - 'Unhandled error Exception: fatal exception occurred ' - 'in Instance of \'CounterExceptionBloc\'.', - ), - ); - expect(stackTrace, isNotNull); - expect(stackTrace, isNot(StackTrace.empty)); - }); - }); - - test('addError triggers onError', () async { - final expectedError = Exception('fatal exception'); - - runZonedGuarded(() { - OnExceptionBloc( - exception: expectedError, - onErrorCallback: (Object _, StackTrace __) {}, - )..addError(expectedError, StackTrace.current); - }, (Object error, StackTrace stackTrace) { - expect( - (error as BlocUnhandledErrorException).toString(), - contains( - 'Unhandled error Exception: fatal exception occurred ' - 'in Instance of \'OnExceptionBloc\'.', - ), - ); - expect(stackTrace, isNotNull); - expect(stackTrace, isNot(StackTrace.empty)); - }); - }); - - test('triggers onError from mapEventToState', () { - runZonedGuarded(() { - final exception = Exception('fatal exception'); - Object? expectedError; - StackTrace? expectedStacktrace; - - final onExceptionBloc = OnExceptionBloc( - exception: exception, - onErrorCallback: (Object error, StackTrace stackTrace) { - expectedError = error; - expectedStacktrace = stackTrace; - }); - - expectLater( - onExceptionBloc.stream, - emitsInOrder([emitsDone]), - ).then((dynamic _) { - expect(expectedError, exception); - expect(expectedStacktrace, isNotNull); - expect(expectedStacktrace, isNot(StackTrace.empty)); - }); - - onExceptionBloc - ..add(CounterEvent.increment) - ..close(); - }, (Object error, StackTrace stackTrace) { - expect( - (error as BlocUnhandledErrorException).toString(), - contains( - 'Unhandled error Exception: fatal exception occurred ' - 'in Instance of \'OnExceptionBloc\'.', - ), - ); - expect(stackTrace, isNotNull); - expect(stackTrace, isNot(StackTrace.empty)); - }); - }); - - test('triggers onError from onEvent', () { - runZonedGuarded(() { - final exception = Exception('fatal exception'); - - OnEventErrorBloc(exception: exception) - ..add(CounterEvent.increment) - ..close(); - }, (Object error, StackTrace stackTrace) { - expect( - (error as BlocUnhandledErrorException).toString(), - contains( - 'Unhandled error Exception: fatal exception occurred ' - 'in Instance of \'OnEventErrorBloc\'.', - ), - ); - expect(stackTrace, isNotNull); - expect(stackTrace, isNot(StackTrace.empty)); - }); - }); - - test('does not trigger onError from add', () { - runZonedGuarded(() { - Object? capturedError; - StackTrace? capturedStacktrace; - final counterBloc = CounterBloc( - onErrorCallback: (error, stackTrace) { - capturedError = error; - capturedStacktrace = stackTrace; - }, - ); - - expectLater( - counterBloc.stream, - emitsInOrder([emitsDone]), - ).then((dynamic _) { - expect(capturedError, isNull); - expect(capturedStacktrace, isNull); - }); - - counterBloc - ..close() - ..add(CounterEvent.increment); - }, (Object _, StackTrace __) { - fail('should not throw when add is called after bloc is closed'); - }); - }); - }); - - group('Error', () { - test('does not break stream', () { - runZonedGuarded( - () { - final expectedStates = [-1, emitsDone]; - final counterBloc = CounterErrorBloc(); - - expectLater(counterBloc.stream, emitsInOrder(expectedStates)); - - counterBloc - ..add(CounterEvent.increment) - ..add(CounterEvent.decrement) - ..close(); - }, - (Object _, StackTrace __) {}, - ); - }); - - test('triggers onError from mapEventToState', () { - runZonedGuarded( - () { - final error = Error(); - Object? expectedError; - StackTrace? expectedStacktrace; - - final onErrorBloc = OnErrorBloc( - error: error, - onErrorCallback: (Object error, StackTrace stackTrace) { - expectedError = error; - expectedStacktrace = stackTrace; - }, - ); - - expectLater( - onErrorBloc.stream, - emitsInOrder([emitsDone]), - ).then((dynamic _) { - expect(expectedError, error); - expect(expectedStacktrace, isNotNull); - }); - - onErrorBloc - ..add(CounterEvent.increment) - ..close(); - }, - (Object _, StackTrace __) {}, - ); - }); - - test('triggers onError from onTransition', () { - runZonedGuarded( - () { - final error = Error(); - Object? expectedError; - StackTrace? expectedStacktrace; - - final onTransitionErrorBloc = OnTransitionErrorBloc( - error: error, - onErrorCallback: (Object error, StackTrace stackTrace) { - expectedError = error; - expectedStacktrace = stackTrace; - }, - ); - - expectLater( - onTransitionErrorBloc.stream, - emitsInOrder([emitsDone]), - ).then((dynamic _) { - expect(expectedError, error); - expect(expectedStacktrace, isNotNull); - expect(onTransitionErrorBloc.state, 0); - }); - - onTransitionErrorBloc - ..add(CounterEvent.increment) - ..close(); - }, - (Object _, StackTrace __) {}, - ); - }); - }); - - group('emit', () { - test('updates the state', () async { - final counterBloc = CounterBloc(); - unawaited( - expectLater(counterBloc.stream, emitsInOrder(const [42])), - ); - counterBloc.emit(42); - expect(counterBloc.state, 42); - await counterBloc.close(); - }); - }); - - group('close', () { - test('emits done (sync)', () { - final bloc = CounterBloc()..close(); - expect(bloc.stream, emitsDone); - }); - - test('emits done (async)', () async { - final bloc = CounterBloc(); - await bloc.close(); - expect(bloc.stream, emitsDone); - }); - }); - - group('isClosed', () { - test('returns true after bloc is closed', () async { - final bloc = CounterBloc(); - expect(bloc.isClosed, isFalse); - await bloc.close(); - expect(bloc.isClosed, isTrue); - }); - }); - }); -} - -void unawaited(Future future) {} diff --git a/packages/bloc/test/legacy/blocs/async/async_bloc.dart b/packages/bloc/test/legacy/blocs/async/async_bloc.dart deleted file mode 100644 index 9a91ab54a52..00000000000 --- a/packages/bloc/test/legacy/blocs/async/async_bloc.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:meta/meta.dart'; - -part 'async_event.dart'; -part 'async_state.dart'; - -class AsyncBloc extends Bloc { - AsyncBloc() : super(AsyncState.initial()); - - @override - Stream mapEventToState(AsyncEvent event) async* { - yield state.copyWith(isLoading: true); - await Future.delayed(const Duration(milliseconds: 500)); - yield state.copyWith(isLoading: false, isSuccess: true); - } -} diff --git a/packages/bloc/test/legacy/blocs/async/async_event.dart b/packages/bloc/test/legacy/blocs/async/async_event.dart deleted file mode 100644 index a2e775a23c8..00000000000 --- a/packages/bloc/test/legacy/blocs/async/async_event.dart +++ /dev/null @@ -1,20 +0,0 @@ -part of 'async_bloc.dart'; - -@immutable -class AsyncEvent { - @override - bool operator ==( - Object other, - ) => - identical( - this, - other, - ) || - other is AsyncEvent && runtimeType == other.runtimeType; - - @override - int get hashCode => runtimeType.hashCode; - - @override - String toString() => 'AsyncEvent'; -} diff --git a/packages/bloc/test/legacy/blocs/async/async_state.dart b/packages/bloc/test/legacy/blocs/async/async_state.dart deleted file mode 100644 index c7cc8cb1a62..00000000000 --- a/packages/bloc/test/legacy/blocs/async/async_state.dart +++ /dev/null @@ -1,48 +0,0 @@ -part of 'async_bloc.dart'; - -@immutable -class AsyncState { - AsyncState({ - required this.isLoading, - required this.hasError, - required this.isSuccess, - }); - - factory AsyncState.initial() { - return AsyncState(isLoading: false, hasError: false, isSuccess: false); - } - - final bool isLoading; - final bool hasError; - final bool isSuccess; - - AsyncState copyWith({bool? isLoading, bool? hasError, bool? isSuccess}) { - return AsyncState( - isLoading: isLoading ?? this.isLoading, - hasError: hasError ?? this.hasError, - isSuccess: isSuccess ?? this.isSuccess); - } - - @override - bool operator ==( - Object other, - ) => - identical( - this, - other, - ) || - other is AsyncState && - runtimeType == other.runtimeType && - isLoading == other.isLoading && - hasError == other.hasError && - isSuccess == other.isSuccess; - - @override - int get hashCode => - isLoading.hashCode ^ hasError.hashCode ^ isSuccess.hashCode; - - @override - String toString() => - 'AsyncState { isLoading: $isLoading, hasError: $hasError, ' - 'isSuccess: $isSuccess }'; -} diff --git a/packages/bloc/test/legacy/blocs/blocs.dart b/packages/bloc/test/legacy/blocs/blocs.dart deleted file mode 100644 index b44f7a358ba..00000000000 --- a/packages/bloc/test/legacy/blocs/blocs.dart +++ /dev/null @@ -1,5 +0,0 @@ -export './async/async_bloc.dart'; -export './complex/complex_bloc.dart'; -export './counter/counter.dart'; -export './seeded/seeded_bloc.dart'; -export './simple/simple_bloc.dart'; diff --git a/packages/bloc/test/legacy/blocs/complex/complex_bloc.dart b/packages/bloc/test/legacy/blocs/complex/complex_bloc.dart deleted file mode 100644 index d3c0b1ec479..00000000000 --- a/packages/bloc/test/legacy/blocs/complex/complex_bloc.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:meta/meta.dart'; -import 'package:stream_transform/stream_transform.dart'; - -part 'complex_event.dart'; -part 'complex_state.dart'; - -class ComplexBloc extends Bloc { - ComplexBloc() : super(ComplexStateA()); - - @override - Stream> transformEvents( - Stream events, - // ignore: deprecated_member_use_from_same_package - TransitionFunction transitionFn, - ) { - return events.switchMap(transitionFn); - } - - @override - Stream mapEventToState(ComplexEvent event) async* { - if (event is ComplexEventA) { - yield ComplexStateA(); - } else if (event is ComplexEventB) { - yield ComplexStateB(); - } else if (event is ComplexEventC) { - await Future.delayed(const Duration(milliseconds: 100)); - yield ComplexStateC(); - } else if (event is ComplexEventD) { - await Future.delayed(const Duration(milliseconds: 100)); - yield ComplexStateD(); - } - } - - @override - Stream> transformTransitions( - Stream> transitions, - ) { - return transitions.debounce(const Duration(milliseconds: 50)); - } -} diff --git a/packages/bloc/test/legacy/blocs/complex/complex_event.dart b/packages/bloc/test/legacy/blocs/complex/complex_event.dart deleted file mode 100644 index 004fe9f6c1d..00000000000 --- a/packages/bloc/test/legacy/blocs/complex/complex_event.dart +++ /dev/null @@ -1,64 +0,0 @@ -part of 'complex_bloc.dart'; - -@immutable -abstract class ComplexEvent {} - -class ComplexEventA extends ComplexEvent { - @override - bool operator ==( - Object other, - ) => - identical( - this, - other, - ) || - other is ComplexEventA && runtimeType == other.runtimeType; - - @override - int get hashCode => 0; -} - -class ComplexEventB extends ComplexEvent { - @override - bool operator ==( - Object other, - ) => - identical( - this, - other, - ) || - other is ComplexEventB && runtimeType == other.runtimeType; - - @override - int get hashCode => 1; -} - -class ComplexEventC extends ComplexEvent { - @override - bool operator ==( - Object other, - ) => - identical( - this, - other, - ) || - other is ComplexEventC && runtimeType == other.runtimeType; - - @override - int get hashCode => 2; -} - -class ComplexEventD extends ComplexEvent { - @override - bool operator ==( - Object other, - ) => - identical( - this, - other, - ) || - other is ComplexEventD && runtimeType == other.runtimeType; - - @override - int get hashCode => 3; -} diff --git a/packages/bloc/test/legacy/blocs/complex/complex_state.dart b/packages/bloc/test/legacy/blocs/complex/complex_state.dart deleted file mode 100644 index a9f198e4817..00000000000 --- a/packages/bloc/test/legacy/blocs/complex/complex_state.dart +++ /dev/null @@ -1,64 +0,0 @@ -part of 'complex_bloc.dart'; - -@immutable -abstract class ComplexState {} - -class ComplexStateA extends ComplexState { - @override - bool operator ==( - Object other, - ) => - identical( - this, - other, - ) || - other is ComplexStateA && runtimeType == other.runtimeType; - - @override - int get hashCode => 0; -} - -class ComplexStateB extends ComplexState { - @override - bool operator ==( - Object other, - ) => - identical( - this, - other, - ) || - other is ComplexStateB && runtimeType == other.runtimeType; - - @override - int get hashCode => 1; -} - -class ComplexStateC extends ComplexState { - @override - bool operator ==( - Object other, - ) => - identical( - this, - other, - ) || - other is ComplexStateC && runtimeType == other.runtimeType; - - @override - int get hashCode => 2; -} - -class ComplexStateD extends ComplexState { - @override - bool operator ==( - Object other, - ) => - identical( - this, - other, - ) || - other is ComplexStateD && runtimeType == other.runtimeType; - - @override - int get hashCode => 3; -} diff --git a/packages/bloc/test/legacy/blocs/counter/counter.dart b/packages/bloc/test/legacy/blocs/counter/counter.dart deleted file mode 100644 index 96c838b0896..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/counter.dart +++ /dev/null @@ -1,10 +0,0 @@ -export './counter_bloc.dart'; -export './counter_error_bloc.dart'; -export './counter_exception_bloc.dart'; -export './flat_map_bloc.dart'; -export './merge_bloc.dart'; -export './mixed_counter_bloc.dart'; -export './on_error_bloc.dart'; -export './on_event_error_bloc.dart'; -export './on_exception_bloc.dart'; -export './on_transition_error_bloc.dart'; diff --git a/packages/bloc/test/legacy/blocs/counter/counter_bloc.dart b/packages/bloc/test/legacy/blocs/counter/counter_bloc.dart deleted file mode 100644 index b0955535d5e..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/counter_bloc.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -typedef OnEventCallback = Function(CounterEvent); -typedef OnTransitionCallback = Function(Transition); -typedef OnErrorCallback = Function(Object error, StackTrace? stackTrace); - -enum CounterEvent { increment, decrement } - -class CounterBloc extends Bloc { - CounterBloc({ - this.onEventCallback, - this.onTransitionCallback, - this.onErrorCallback, - }) : super(0); - - final OnEventCallback? onEventCallback; - final OnTransitionCallback? onTransitionCallback; - final OnErrorCallback? onErrorCallback; - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.decrement: - yield state - 1; - break; - case CounterEvent.increment: - yield state + 1; - break; - } - } - - @override - void onEvent(CounterEvent event) { - super.onEvent(event); - onEventCallback?.call(event); - } - - @override - void onTransition(Transition transition) { - super.onTransition(transition); - onTransitionCallback?.call(transition); - } - - @override - void onError(Object error, StackTrace stackTrace) { - onErrorCallback?.call(error, stackTrace); - super.onError(error, stackTrace); - } -} diff --git a/packages/bloc/test/legacy/blocs/counter/counter_error_bloc.dart b/packages/bloc/test/legacy/blocs/counter/counter_error_bloc.dart deleted file mode 100644 index 3d93284ecb3..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/counter_error_bloc.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import '../counter/counter_bloc.dart'; - -class CounterErrorBloc extends Bloc { - CounterErrorBloc() : super(0); - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.decrement: - yield state - 1; - break; - case CounterEvent.increment: - throw Error(); - } - } -} diff --git a/packages/bloc/test/legacy/blocs/counter/counter_exception_bloc.dart b/packages/bloc/test/legacy/blocs/counter/counter_exception_bloc.dart deleted file mode 100644 index 73f053fdd43..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/counter_exception_bloc.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import '../counter/counter_bloc.dart'; - -class CounterExceptionBloc extends Bloc { - CounterExceptionBloc() : super(0); - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.decrement: - yield state - 1; - break; - case CounterEvent.increment: - throw Exception('fatal exception'); - } - } -} diff --git a/packages/bloc/test/legacy/blocs/counter/flat_map_bloc.dart b/packages/bloc/test/legacy/blocs/counter/flat_map_bloc.dart deleted file mode 100644 index 9293e3a486f..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/flat_map_bloc.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:stream_transform/stream_transform.dart'; - -import '../blocs.dart'; - -class FlatMapBloc extends Bloc { - FlatMapBloc({this.onTransitionCallback}) : super(0); - - final void Function(Transition)? onTransitionCallback; - - @override - void onTransition(Transition transition) { - super.onTransition(transition); - onTransitionCallback?.call(transition); - } - - @override - Stream> transformEvents( - Stream events, - // ignore: deprecated_member_use_from_same_package - TransitionFunction transitionFn, - ) { - return events.concurrentAsyncExpand(transitionFn); - } - - @override - Stream mapEventToState( - CounterEvent event, - ) async* { - switch (event) { - case CounterEvent.decrement: - yield state - 1; - break; - case CounterEvent.increment: - yield state + 1; - break; - } - } -} diff --git a/packages/bloc/test/legacy/blocs/counter/merge_bloc.dart b/packages/bloc/test/legacy/blocs/counter/merge_bloc.dart deleted file mode 100644 index 7f1501b0d16..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/merge_bloc.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:stream_transform/stream_transform.dart'; - -import '../blocs.dart'; - -class MergeBloc extends Bloc { - MergeBloc({this.onTransitionCallback}) : super(0); - - final void Function(Transition)? onTransitionCallback; - - @override - void onTransition(Transition transition) { - super.onTransition(transition); - onTransitionCallback?.call(transition); - } - - @override - Stream> transformEvents( - Stream events, - // ignore: deprecated_member_use_from_same_package - TransitionFunction transitionFn, - ) { - final nonDebounceStream = - events.where((event) => event != CounterEvent.increment); - - final debounceStream = events - .where((event) => event == CounterEvent.increment) - .throttle(const Duration(milliseconds: 100)); - - return nonDebounceStream - .merge(debounceStream) - .concurrentAsyncExpand(transitionFn); - } - - @override - Stream mapEventToState( - CounterEvent event, - ) async* { - switch (event) { - case CounterEvent.decrement: - yield state - 1; - break; - case CounterEvent.increment: - yield state + 1; - break; - } - } -} diff --git a/packages/bloc/test/legacy/blocs/counter/mixed_counter_bloc.dart b/packages/bloc/test/legacy/blocs/counter/mixed_counter_bloc.dart deleted file mode 100644 index 68cb42533e7..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/mixed_counter_bloc.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import '../counter/counter_bloc.dart'; - -class MixedCounterBloc extends Bloc { - MixedCounterBloc() : super(0) { - on((event, emit) => emit(_mapEventToState(event))); - } - - @override - Stream mapEventToState(CounterEvent event) async* { - yield _mapEventToState(event); - } - - int _mapEventToState(CounterEvent event) { - switch (event) { - case CounterEvent.increment: - return state + 1; - case CounterEvent.decrement: - return state - 1; - } - } -} diff --git a/packages/bloc/test/legacy/blocs/counter/on_error_bloc.dart b/packages/bloc/test/legacy/blocs/counter/on_error_bloc.dart deleted file mode 100644 index 7cf3587d8b8..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/on_error_bloc.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import '../counter/counter_bloc.dart'; - -class OnErrorBloc extends Bloc { - OnErrorBloc({required this.error, required this.onErrorCallback}) : super(0); - - final Function onErrorCallback; - final Error error; - - @override - void onError(Object error, StackTrace stackTrace) { - onErrorCallback(error, stackTrace); - super.onError(error, stackTrace); - } - - @override - Stream mapEventToState(CounterEvent event) async* { - throw error; - } -} diff --git a/packages/bloc/test/legacy/blocs/counter/on_event_error_bloc.dart b/packages/bloc/test/legacy/blocs/counter/on_event_error_bloc.dart deleted file mode 100644 index 7596eda6ef3..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/on_event_error_bloc.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import '../counter/counter_bloc.dart'; - -class OnEventErrorBloc extends Bloc { - OnEventErrorBloc({required this.exception}) : super(0); - - final Exception exception; - - @override - // ignore: must_call_super - void onEvent(CounterEvent event) { - throw exception; - } - - @override - Stream mapEventToState(CounterEvent event) async* {} -} diff --git a/packages/bloc/test/legacy/blocs/counter/on_exception_bloc.dart b/packages/bloc/test/legacy/blocs/counter/on_exception_bloc.dart deleted file mode 100644 index 8a2396b449a..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/on_exception_bloc.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import '../counter/counter_bloc.dart'; - -class OnExceptionBloc extends Bloc { - OnExceptionBloc({ - required this.exception, - required this.onErrorCallback, - }) : super(0); - - final Function onErrorCallback; - final Exception exception; - - @override - void onError(Object error, StackTrace stackTrace) { - onErrorCallback(error, stackTrace); - super.onError(error, stackTrace); - } - - @override - Stream mapEventToState(CounterEvent event) async* { - throw exception; - } -} diff --git a/packages/bloc/test/legacy/blocs/counter/on_transition_error_bloc.dart b/packages/bloc/test/legacy/blocs/counter/on_transition_error_bloc.dart deleted file mode 100644 index be9ec166e79..00000000000 --- a/packages/bloc/test/legacy/blocs/counter/on_transition_error_bloc.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import '../counter/counter_bloc.dart'; - -class OnTransitionErrorBloc extends Bloc { - OnTransitionErrorBloc({ - required this.error, - required this.onErrorCallback, - }) : super(0); - - final Function onErrorCallback; - final Error error; - - @override - void onError(Object error, StackTrace stackTrace) { - onErrorCallback(error, stackTrace); - super.onError(error, stackTrace); - } - - @override - void onTransition(Transition transition) { - super.onTransition(transition); - throw error; - } - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.increment: - yield state + 1; - break; - case CounterEvent.decrement: - yield state - 1; - break; - } - } -} diff --git a/packages/bloc/test/legacy/blocs/seeded/seeded_bloc.dart b/packages/bloc/test/legacy/blocs/seeded/seeded_bloc.dart deleted file mode 100644 index 99b0b60784f..00000000000 --- a/packages/bloc/test/legacy/blocs/seeded/seeded_bloc.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:bloc/bloc.dart'; - -class SeededBloc extends Bloc { - SeededBloc({required this.seed, required this.states}) : super(seed); - - final List states; - final int seed; - - @override - Stream mapEventToState(String event) async* { - for (final state in states) { - yield state; - } - } -} diff --git a/packages/bloc/test/legacy/blocs/simple/simple_bloc.dart b/packages/bloc/test/legacy/blocs/simple/simple_bloc.dart deleted file mode 100644 index 3bbc217658b..00000000000 --- a/packages/bloc/test/legacy/blocs/simple/simple_bloc.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -class SimpleBloc extends Bloc { - SimpleBloc() : super(''); - - @override - Stream mapEventToState(dynamic event) async* { - yield 'data'; - } -} diff --git a/packages/bloc_concurrency/CHANGELOG.md b/packages/bloc_concurrency/CHANGELOG.md index d4b06472cdf..4946494a4fa 100644 --- a/packages/bloc_concurrency/CHANGELOG.md +++ b/packages/bloc_concurrency/CHANGELOG.md @@ -1,3 +1,11 @@ +# 0.2.0-dev.2 + +- feat: upgrade to `bloc: ^8.0.0-dev.5` + +# 0.2.0-dev.1 + +- feat: upgrade to `bloc: ^8.0.0-dev.3` + # 0.1.0 - feat: upgrade to `bloc: ^7.2.0` diff --git a/packages/bloc_concurrency/analysis_options.yaml b/packages/bloc_concurrency/analysis_options.yaml index 4bc4f9710c2..bf25477f4aa 100644 --- a/packages/bloc_concurrency/analysis_options.yaml +++ b/packages/bloc_concurrency/analysis_options.yaml @@ -1,4 +1,4 @@ -include: package:very_good_analysis/analysis_options.2.3.0.yaml +include: package:very_good_analysis/analysis_options.2.4.0.yaml analyzer: exclude: - "**/version.dart" diff --git a/packages/bloc_concurrency/pubspec.yaml b/packages/bloc_concurrency/pubspec.yaml index 13ac65bd0eb..9126d405b94 100644 --- a/packages/bloc_concurrency/pubspec.yaml +++ b/packages/bloc_concurrency/pubspec.yaml @@ -1,6 +1,6 @@ name: bloc_concurrency description: Custom event transformers inspired by ember concurrency. Built to be used with the bloc state management package. -version: 0.1.0 +version: 0.2.0-dev.2 repository: https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency issue_tracker: https://github.com/felangel/bloc/issues homepage: https://github.com/felangel/bloc @@ -10,10 +10,10 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - bloc: ^7.2.0 + bloc: ^8.0.0-dev.5 stream_transform: ^2.0.0 dev_dependencies: - mocktail: ^0.1.1 + mocktail: ^0.2.0 test: ^1.17.0 - very_good_analysis: ^2.3.0 + very_good_analysis: ^2.4.0 diff --git a/packages/bloc_test/CHANGELOG.md b/packages/bloc_test/CHANGELOG.md index 30c9d95b0f1..8b3a11bb573 100644 --- a/packages/bloc_test/CHANGELOG.md +++ b/packages/bloc_test/CHANGELOG.md @@ -1,3 +1,24 @@ +# 9.0.0-dev.5 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.5` + +# 9.0.0-dev.4 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.4` + +# 9.0.0-dev.3 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.3` + +# 9.0.0-dev.2 + +- **BREAKING**: feat: upgrade to `mocktail v0.2.0` + +# 9.0.0-dev.1 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.2` +- feat: `MockBloc` no longer implicitly requires `registerFallbackValue` for events and states + # 8.5.0 - feat: prettier diffing when using `blocTest` and `expect` does not match emitted states ([#1783](https://github.com/felangel/bloc/issues/1783)) diff --git a/packages/bloc_test/example/main.dart b/packages/bloc_test/example/main.dart index 5fec15dca55..523c49d116d 100644 --- a/packages/bloc_test/example/main.dart +++ b/packages/bloc_test/example/main.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; // Mock Cubit @@ -12,14 +11,7 @@ class MockCounterCubit extends MockCubit implements CounterCubit {} class MockCounterBloc extends MockBloc implements CounterBloc {} -// Fake Counter Event -class FakeCounterEvent extends Fake implements CounterEvent {} - void main() { - setUpAll(() { - registerFallbackValue(FakeCounterEvent()); - }); - mainCubit(); mainBloc(); } diff --git a/packages/bloc_test/lib/src/bloc_test.dart b/packages/bloc_test/lib/src/bloc_test.dart index 84b7e79ccf4..bbab3497e97 100644 --- a/packages/bloc_test/lib/src/bloc_test.dart +++ b/packages/bloc_test/lib/src/bloc_test.dart @@ -185,62 +185,78 @@ Future testBloc, State>({ dynamic Function()? errors, FutureOr Function()? tearDown, }) async { - final unhandledErrors = []; var shallowEquality = false; - await runZonedGuarded( + final unhandledErrors = []; + await BlocOverrides.runZoned( () async { - await setUp?.call(); - final states = []; - final bloc = build(); - // ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member - if (seed != null) bloc.emit(seed()); - final subscription = bloc.stream.skip(skip).listen(states.add); - try { - await act?.call(bloc); - } catch (error) { - unhandledErrors.add( - error is BlocUnhandledErrorException ? error.error : error, - ); - } - if (wait != null) await Future.delayed(wait); - await Future.delayed(Duration.zero); - await bloc.close(); - if (expect != null) { - final dynamic expected = expect(); - shallowEquality = '$states' == '$expected'; - try { - test.expect(states, test.wrapMatcher(expected)); - } on test.TestFailure catch (e) { - if (shallowEquality || expected is! List) rethrow; - final diff = _diff(expected: expected, actual: states); - final message = '${e.message}\n$diff'; - // ignore: only_throw_errors - throw test.TestFailure(message); - } - } - await subscription.cancel(); - await verify?.call(bloc); - await tearDown?.call(); - }, - (Object error, _) { - if (error is BlocUnhandledErrorException) { - unhandledErrors.add(error.error); - } else if (shallowEquality && error is test.TestFailure) { - // ignore: only_throw_errors - throw test.TestFailure( - '''${error.message} + await runZonedGuarded( + () async { + await setUp?.call(); + final states = []; + final bloc = build(); + // ignore: invalid_use_of_protected_member + if (seed != null) bloc.emit(seed()); + final subscription = bloc.stream.skip(skip).listen(states.add); + try { + await act?.call(bloc); + } catch (error) { + unhandledErrors.add(error); + } + if (wait != null) await Future.delayed(wait); + await Future.delayed(Duration.zero); + await bloc.close(); + if (expect != null) { + final dynamic expected = expect(); + shallowEquality = '$states' == '$expected'; + try { + test.expect(states, test.wrapMatcher(expected)); + } on test.TestFailure catch (e) { + if (shallowEquality || expected is! List) rethrow; + final diff = _diff(expected: expected, actual: states); + final message = '${e.message}\n$diff'; + // ignore: only_throw_errors + throw test.TestFailure(message); + } + } + await subscription.cancel(); + await verify?.call(bloc); + await tearDown?.call(); + }, + (Object error, _) { + if (shallowEquality && error is test.TestFailure) { + // ignore: only_throw_errors + throw test.TestFailure( + '''${error.message} WARNING: Please ensure state instances extend Equatable, override == and hashCode, or implement Comparable. Alternatively, consider using Matchers in the expect of the blocTest rather than concrete state instances.\n''', - ); - } else { - // ignore: only_throw_errors - throw error; - } + ); + } + if (!unhandledErrors.contains(error)) { + // ignore: only_throw_errors + throw error; + } + }, + ); }, + blocObserver: _TestBlocObserver(unhandledErrors.add), ); if (errors != null) test.expect(unhandledErrors, test.wrapMatcher(errors())); } +class _TestBlocObserver extends BlocObserver { + _TestBlocObserver(this._onError); + + final _localObserver = BlocOverrides.current?.blocObserver; + final void Function(Object error) _onError; + + @override + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { + _localObserver?.onError(bloc, error, stackTrace); + _onError(error); + super.onError(bloc, error, stackTrace); + } +} + String _diff({required dynamic expected, required dynamic actual}) { final buffer = StringBuffer(); final differences = diff(expected.toString(), actual.toString()); diff --git a/packages/bloc_test/lib/src/mock_bloc.dart b/packages/bloc_test/lib/src/mock_bloc.dart index 404152abd9a..46a1338cdb7 100644 --- a/packages/bloc_test/lib/src/mock_bloc.dart +++ b/packages/bloc_test/lib/src/mock_bloc.dart @@ -23,14 +23,7 @@ import 'package:mocktail/mocktail.dart'; /// class MockCounterBloc extends MockBloc implements CounterBloc {} /// ``` /// {@endtemplate} -class MockBloc extends _MockBlocBase implements Bloc { - /// {@macro mock_bloc} - MockBloc() { - // ignore: deprecated_member_use - when(() => mapEventToState(any())).thenAnswer((_) => Stream.empty()); - when(() => add(any())).thenReturn(null); - } -} +class MockBloc extends _MockBlocBase implements Bloc {} /// {@template mock_cubit} /// Extend or mixin this class to mark the implementation as a [MockCubit]. @@ -58,26 +51,7 @@ class MockCubit extends _MockBlocBase implements Cubit {} class _MockBlocBase extends Mock implements BlocBase { _MockBlocBase() { - registerFallbackValue((S _) {}); - registerFallbackValue(() {}); - when( - // ignore: deprecated_member_use - () => listen( - any(), - onDone: any(named: 'onDone'), - onError: any(named: 'onError'), - cancelOnError: any(named: 'cancelOnError'), - ), - ).thenAnswer((invocation) { - return Stream.empty().listen( - invocation.positionalArguments.first as void Function(S data), - onError: invocation.namedArguments[#onError] as Function?, - onDone: invocation.namedArguments[#onDone] as void Function()?, - cancelOnError: invocation.namedArguments[#cancelOnError] as bool?, - ); - }); when(() => stream).thenAnswer((_) => Stream.empty()); when(close).thenAnswer((_) => Future.value()); - when(() => emit(any())).thenReturn(null); } } diff --git a/packages/bloc_test/lib/src/when_listen.dart b/packages/bloc_test/lib/src/when_listen.dart index 6ab9d6e9d39..87a0df2fdef 100644 --- a/packages/bloc_test/lib/src/when_listen.dart +++ b/packages/bloc_test/lib/src/when_listen.dart @@ -52,26 +52,6 @@ void whenListen( when(() => bloc.state).thenReturn(initialState); } - when( - // ignore: deprecated_member_use - () => bloc.listen( - any(), - onDone: any(named: 'onDone'), - onError: any(named: 'onError'), - cancelOnError: any(named: 'cancelOnError'), - ), - ).thenAnswer((invocation) { - return broadcastStream.listen( - (state) { - when(() => bloc.state).thenReturn(state); - (invocation.positionalArguments.first as Function(State)).call(state); - }, - onError: invocation.namedArguments[#onError] as Function?, - onDone: invocation.namedArguments[#onDone] as void Function()?, - cancelOnError: invocation.namedArguments[#cancelOnError] as bool?, - ); - }); - when(() => bloc.stream).thenAnswer( (_) => broadcastStream.map((state) { when(() => bloc.state).thenReturn(state); diff --git a/packages/bloc_test/pubspec.yaml b/packages/bloc_test/pubspec.yaml index d7695b6e98e..b4b6eece42f 100644 --- a/packages/bloc_test/pubspec.yaml +++ b/packages/bloc_test/pubspec.yaml @@ -1,6 +1,6 @@ name: bloc_test description: A testing library which makes it easy to test blocs. Built to be used with the bloc state management package. -version: 8.5.0 +version: 9.0.0-dev.5 repository: https://github.com/felangel/bloc/tree/master/packages/bloc_test issue_tracker: https://github.com/felangel/bloc/issues homepage: https://bloclibrary.dev @@ -10,12 +10,11 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - bloc: ^7.2.0 + bloc: ^8.0.0-dev.5 diff_match_patch: ^0.4.1 meta: ^1.3.0 mocktail: ^0.2.0 test: ^1.16.0 dev_dependencies: - pedantic: ^1.10.0 - rxdart: ^0.26.0 + rxdart: ^0.27.2 diff --git a/packages/bloc_test/test/bloc_bloc_test_test.dart b/packages/bloc_test/test/bloc_bloc_test_test.dart index 3529b6accae..76b49990393 100644 --- a/packages/bloc_test/test/bloc_bloc_test_test.dart +++ b/packages/bloc_test/test/bloc_bloc_test_test.dart @@ -2,13 +2,14 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:pedantic/pedantic.dart'; import 'package:test/test.dart'; import 'blocs/blocs.dart'; class MockRepository extends Mock implements Repository {} +void unawaited(Future? _) {} + void main() { group('blocTest', () { group('CounterBloc', () { @@ -86,7 +87,8 @@ void main() { blocTest( 'emits [11] when CounterEvent.increment is added and emitted 10', - build: () => CounterBloc()..emit(10), + build: () => CounterBloc(), + seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [11], ); @@ -165,7 +167,8 @@ void main() { blocTest( 'emits [11] when CounterEvent.increment is added and emitted 10', - build: () => AsyncCounterBloc()..emit(10), + build: () => AsyncCounterBloc(), + seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [11], ); @@ -202,14 +205,15 @@ void main() { blocTest( 'emits [11] when CounterEvent.increment is added and emitted 10', - build: () => DebounceCounterBloc()..emit(10), + build: () => DebounceCounterBloc(), + seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), wait: const Duration(milliseconds: 300), expect: () => const [11], ); }); - group('InstanceEmitBloc', () { + group('InstantEmitBloc', () { blocTest( 'emits [1] when nothing is added', build: () => InstantEmitBloc(), @@ -246,8 +250,9 @@ void main() { ); blocTest( - 'emits [11, 12] when CounterEvent.increment is added and emitted 10', - build: () => InstantEmitBloc()..emit(10), + 'emits [11, 12] when CounterEvent.increment is added and seeded 10', + build: () => InstantEmitBloc(), + seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [11, 12], ); @@ -291,7 +296,8 @@ void main() { blocTest( 'emits [11, 12] when CounterEvent.increment is added and emitted 10', - build: () => MultiCounterBloc()..emit(10), + build: () => MultiCounterBloc(), + seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [11, 12], ); diff --git a/packages/bloc_test/test/legacy/bloc_bloc_test_test.dart b/packages/bloc_test/test/legacy/bloc_bloc_test_test.dart deleted file mode 100644 index 3529b6accae..00000000000 --- a/packages/bloc_test/test/legacy/bloc_bloc_test_test.dart +++ /dev/null @@ -1,614 +0,0 @@ -import 'dart:async'; - -import 'package:bloc_test/bloc_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pedantic/pedantic.dart'; -import 'package:test/test.dart'; - -import 'blocs/blocs.dart'; - -class MockRepository extends Mock implements Repository {} - -void main() { - group('blocTest', () { - group('CounterBloc', () { - blocTest( - 'supports matchers (contains)', - build: () => CounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => contains(1), - ); - - blocTest( - 'supports matchers (containsAll)', - build: () => CounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - expect: () => containsAll([2, 1]), - ); - - blocTest( - 'supports matchers (containsAllInOrder)', - build: () => CounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - expect: () => containsAllInOrder([1, 2]), - ); - - blocTest( - 'emits [] when nothing is added', - build: () => CounterBloc(), - expect: () => const [], - ); - - blocTest( - 'emits [1] when CounterEvent.increment is added', - build: () => CounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [1], - ); - - blocTest( - 'emits [1] when CounterEvent.increment is added with async act', - build: () => CounterBloc(), - act: (bloc) async { - await Future.delayed(const Duration(seconds: 1)); - bloc.add(CounterEvent.increment); - }, - expect: () => const [1], - ); - - blocTest( - 'emits [1, 2] when CounterEvent.increment is called multiple times ' - 'with async act', - build: () => CounterBloc(), - act: (bloc) async { - bloc.add(CounterEvent.increment); - await Future.delayed(const Duration(milliseconds: 10)); - bloc.add(CounterEvent.increment); - }, - expect: () => const [1, 2], - ); - - blocTest( - 'emits [2] when CounterEvent.increment is added twice and skip: 1', - build: () => CounterBloc(), - act: (bloc) { - bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment); - }, - skip: 1, - expect: () => const [2], - ); - - blocTest( - 'emits [11] when CounterEvent.increment is added and emitted 10', - build: () => CounterBloc()..emit(10), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [11], - ); - - blocTest( - 'emits [11] when CounterEvent.increment is added and seed 10', - build: () => CounterBloc(), - seed: () => 10, - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [11], - ); - - test('fails immediately when expectation is incorrect', () async { - const expectedError = 'Expected: [2]\n' - ' Actual: [1]\n' - ' Which: at location [0] is <1> instead of <2>\n' - '\n' - '==== diff ========================================\n' - '\n' - '''\x1B[90m[\x1B[0m\x1B[31m[-2-]\x1B[0m\x1B[32m{+1+}\x1B[0m\x1B[90m]\x1B[0m\n''' - '\n' - '==== end diff ====================================\n' - ''; - late Object actualError; - final completer = Completer(); - await runZonedGuarded(() async { - unawaited(testBloc( - build: () => CounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [2], - ).then((_) => completer.complete())); - await completer.future; - }, (Object error, _) { - actualError = error; - completer.complete(); - }); - expect((actualError as TestFailure).message, expectedError); - }); - }); - - group('AsyncCounterBloc', () { - blocTest( - 'emits [] when nothing is added', - build: () => AsyncCounterBloc(), - expect: () => const [], - ); - - blocTest( - 'emits [1] when CounterEvent.increment is added', - build: () => AsyncCounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [1], - ); - - blocTest( - 'emits [1, 2] when CounterEvent.increment is called multiple' - 'times with async act', - build: () => AsyncCounterBloc(), - act: (bloc) async { - bloc.add(CounterEvent.increment); - await Future.delayed(const Duration(milliseconds: 10)); - bloc.add(CounterEvent.increment); - }, - expect: () => const [1, 2], - ); - - blocTest( - 'emits [2] when CounterEvent.increment is added twice and skip: 1', - build: () => AsyncCounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - skip: 1, - expect: () => const [2], - ); - - blocTest( - 'emits [11] when CounterEvent.increment is added and emitted 10', - build: () => AsyncCounterBloc()..emit(10), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [11], - ); - }); - - group('DebounceCounterBloc', () { - blocTest( - 'emits [] when nothing is added', - build: () => DebounceCounterBloc(), - expect: () => const [], - ); - - blocTest( - 'emits [1] when CounterEvent.increment is added', - build: () => DebounceCounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - wait: const Duration(milliseconds: 300), - expect: () => const [1], - ); - - blocTest( - 'emits [2] when CounterEvent.increment ' - 'is added twice and skip: 1', - build: () => DebounceCounterBloc(), - act: (bloc) async { - bloc.add(CounterEvent.increment); - await Future.delayed(const Duration(milliseconds: 305)); - bloc.add(CounterEvent.increment); - }, - skip: 1, - wait: const Duration(milliseconds: 300), - expect: () => const [2], - ); - - blocTest( - 'emits [11] when CounterEvent.increment is added and emitted 10', - build: () => DebounceCounterBloc()..emit(10), - act: (bloc) => bloc.add(CounterEvent.increment), - wait: const Duration(milliseconds: 300), - expect: () => const [11], - ); - }); - - group('InstanceEmitBloc', () { - blocTest( - 'emits [1] when nothing is added', - build: () => InstantEmitBloc(), - expect: () => const [1], - ); - - blocTest( - 'emits [1, 2] when CounterEvent.increment is added', - build: () => InstantEmitBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [1, 2], - ); - - blocTest( - 'emits [1, 2, 3] when CounterEvent.increment is called' - 'multiple times with async act', - build: () => InstantEmitBloc(), - act: (bloc) async { - bloc.add(CounterEvent.increment); - await Future.delayed(const Duration(milliseconds: 10)); - bloc.add(CounterEvent.increment); - }, - expect: () => const [1, 2, 3], - ); - - blocTest( - 'emits [3] when CounterEvent.increment is added twice and skip: 2', - build: () => InstantEmitBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - skip: 2, - expect: () => const [3], - ); - - blocTest( - 'emits [11, 12] when CounterEvent.increment is added and emitted 10', - build: () => InstantEmitBloc()..emit(10), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [11, 12], - ); - }); - - group('MultiCounterBloc', () { - blocTest( - 'emits [] when nothing is added', - build: () => MultiCounterBloc(), - expect: () => const [], - ); - - blocTest( - 'emits [1, 2] when CounterEvent.increment is added', - build: () => MultiCounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [1, 2], - ); - - blocTest( - 'emits [1, 2, 3, 4] when CounterEvent.increment is called' - 'multiple times with async act', - build: () => MultiCounterBloc(), - act: (bloc) async { - bloc.add(CounterEvent.increment); - await Future.delayed(const Duration(milliseconds: 10)); - bloc.add(CounterEvent.increment); - }, - expect: () => const [1, 2, 3, 4], - ); - - blocTest( - 'emits [4] when CounterEvent.increment is added twice and skip: 3', - build: () => MultiCounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - skip: 3, - expect: () => const [4], - ); - - blocTest( - 'emits [11, 12] when CounterEvent.increment is added and emitted 10', - build: () => MultiCounterBloc()..emit(10), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [11, 12], - ); - }); - - group('ComplexBloc', () { - blocTest( - 'emits [] when nothing is added', - build: () => ComplexBloc(), - expect: () => const [], - ); - - blocTest( - 'emits [ComplexStateB] when ComplexEventB is added', - build: () => ComplexBloc(), - act: (bloc) => bloc.add(ComplexEventB()), - expect: () => [isA()], - ); - - blocTest( - 'emits [ComplexStateA] when [ComplexEventB, ComplexEventA] ' - 'is added and skip: 1', - build: () => ComplexBloc(), - act: (bloc) => bloc - ..add(ComplexEventB()) - ..add(ComplexEventA()), - skip: 1, - expect: () => [isA()], - ); - }); - group('ErrorCounterBloc', () { - blocTest( - 'emits [] when nothing is added', - build: () => ErrorCounterBloc(), - expect: () => const [], - ); - - blocTest( - 'emits [2] when increment is added twice and skip: 1', - build: () => ErrorCounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - skip: 1, - expect: () => const [2], - ); - - blocTest( - 'emits [1] when increment is added', - build: () => ErrorCounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [1], - ); - - blocTest( - 'throws ErrorCounterBlocException when increment is added', - build: () => ErrorCounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - errors: () => [isA()], - ); - - blocTest( - 'emits [1] and throws ErrorCounterBlocError ' - 'when increment is added', - build: () => ErrorCounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [1], - errors: () => [isA()], - ); - - blocTest( - 'emits [1, 2] when increment is added twice', - build: () => ErrorCounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - expect: () => const [1, 2], - ); - - blocTest( - 'throws two ErrorCounterBlocErrors ' - 'when increment is added twice', - build: () => ErrorCounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - errors: () => [ - isA(), - isA(), - ], - ); - - blocTest( - 'emits [1, 2] and throws two ErrorCounterBlocErrors ' - 'when increment is added twice', - build: () => ErrorCounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - expect: () => const [1, 2], - errors: () => [ - isA(), - isA(), - ], - ); - }); - - group('ExceptionCounterBloc', () { - blocTest( - 'emits [] when nothing is added', - build: () => ExceptionCounterBloc(), - expect: () => const [], - ); - - blocTest( - 'emits [2] when increment is added twice and skip: 1', - build: () => ExceptionCounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - skip: 1, - expect: () => const [2], - ); - - blocTest( - 'emits [1] when increment is added', - build: () => ExceptionCounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [1], - ); - - blocTest( - 'throws ExceptionCounterBlocException when increment is added', - build: () => ExceptionCounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - errors: () => [isA()], - ); - - blocTest( - 'emits [1] and throws ExceptionCounterBlocException ' - 'when increment is added', - build: () => ExceptionCounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [1], - errors: () => [isA()], - ); - - blocTest( - 'emits [1, 2] when increment is added twice', - build: () => ExceptionCounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - expect: () => const [1, 2], - ); - - blocTest( - 'throws two ExceptionCounterBlocExceptions ' - 'when increment is added twice', - build: () => ExceptionCounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - errors: () => [ - isA(), - isA(), - ], - ); - - blocTest( - 'emits [1, 2] and throws two ExceptionCounterBlocException ' - 'when increment is added twice', - build: () => ExceptionCounterBloc(), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - expect: () => const [1, 2], - errors: () => [ - isA(), - isA(), - ], - ); - }); - - group('SideEffectCounterBloc', () { - late Repository repository; - - setUp(() { - repository = MockRepository(); - when(() => repository.sideEffect()).thenReturn(null); - }); - - blocTest( - 'emits [] when nothing is added', - build: () => SideEffectCounterBloc(repository), - expect: () => const [], - ); - - blocTest( - 'emits [1] when CounterEvent.increment is added', - build: () => SideEffectCounterBloc(repository), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [1], - verify: (_) { - verify(() => repository.sideEffect()).called(1); - }, - ); - - blocTest( - 'emits [2] when CounterEvent.increment ' - 'is added twice and skip: 1', - build: () => SideEffectCounterBloc(repository), - act: (bloc) => bloc - ..add(CounterEvent.increment) - ..add(CounterEvent.increment), - skip: 1, - expect: () => const [2], - ); - - blocTest( - 'does not require an expect', - build: () => SideEffectCounterBloc(repository), - act: (bloc) => bloc.add(CounterEvent.increment), - verify: (_) { - verify(() => repository.sideEffect()).called(1); - }, - ); - - blocTest( - 'async verify', - build: () => SideEffectCounterBloc(repository), - act: (bloc) => bloc.add(CounterEvent.increment), - verify: (_) async { - await Future.delayed(Duration.zero); - verify(() => repository.sideEffect()).called(1); - }, - ); - - blocTest( - 'setUp is executed before build/act', - setUp: () { - when(() => repository.sideEffect()).thenThrow(Exception()); - }, - build: () => SideEffectCounterBloc(repository), - act: (bloc) => bloc.add(CounterEvent.increment), - expect: () => const [], - errors: () => [isException], - ); - - test('fails immediately when verify is incorrect', () async { - const expectedError = - '''Expected: <2>\n Actual: <1>\nUnexpected number of calls\n'''; - late Object actualError; - final completer = Completer(); - await runZonedGuarded(() async { - unawaited(testBloc( - build: () => SideEffectCounterBloc(repository), - act: (bloc) => bloc.add(CounterEvent.increment), - verify: (_) { - verify(() => repository.sideEffect()).called(2); - }, - ).then((_) => completer.complete())); - await completer.future; - }, (Object error, _) { - actualError = error; - completer.complete(); - }); - expect((actualError as TestFailure).message, expectedError); - }); - - test('shows equality warning when strings are identical', () async { - const expectedError = '''Expected: [Instance of \'ComplexStateA\'] - Actual: [Instance of \'ComplexStateA\'] - Which: at location [0] is instead of \n -WARNING: Please ensure state instances extend Equatable, override == and hashCode, or implement Comparable. -Alternatively, consider using Matchers in the expect of the blocTest rather than concrete state instances.\n'''; - late Object actualError; - final completer = Completer(); - await runZonedGuarded(() async { - unawaited(testBloc( - build: () => ComplexBloc(), - act: (bloc) => bloc.add(ComplexEventA()), - expect: () => [ComplexStateA()], - ).then((_) => completer.complete())); - await completer.future; - }, (Object error, _) { - actualError = error; - completer.complete(); - }); - expect((actualError as TestFailure).message, expectedError); - }); - }); - }); - - group('tearDown', () { - late int tearDownCallCount; - int? state; - - setUp(() { - tearDownCallCount = 0; - }); - - tearDown(() { - expect(tearDownCallCount, equals(1)); - }); - - blocTest( - 'is called after the test is run', - build: () => CounterBloc(), - act: (bloc) => bloc.add(CounterEvent.increment), - verify: (bloc) { - state = bloc.state; - }, - tearDown: () { - tearDownCallCount++; - expect(state, equals(1)); - }, - ); - }); -} diff --git a/packages/bloc_test/test/legacy/blocs/async_counter_bloc.dart b/packages/bloc_test/test/legacy/blocs/async_counter_bloc.dart deleted file mode 100644 index f1edb71f18d..00000000000 --- a/packages/bloc_test/test/legacy/blocs/async_counter_bloc.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import 'blocs.dart'; - -class AsyncCounterBloc extends Bloc { - AsyncCounterBloc() : super(0); - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.increment: - await Future.delayed(const Duration(microseconds: 1)); - yield state + 1; - break; - } - } -} diff --git a/packages/bloc_test/test/legacy/blocs/blocs.dart b/packages/bloc_test/test/legacy/blocs/blocs.dart deleted file mode 100644 index b37ee4f24db..00000000000 --- a/packages/bloc_test/test/legacy/blocs/blocs.dart +++ /dev/null @@ -1,10 +0,0 @@ -export 'async_counter_bloc.dart'; -export 'complex_bloc.dart'; -export 'counter_bloc.dart'; -export 'debounce_counter_bloc.dart'; -export 'error_counter_bloc.dart'; -export 'exception_counter_bloc.dart'; -export 'instant_emit_bloc.dart'; -export 'multi_counter_bloc.dart'; -export 'side_effect_counter_bloc.dart'; -export 'sum_bloc.dart'; diff --git a/packages/bloc_test/test/legacy/blocs/complex_bloc.dart b/packages/bloc_test/test/legacy/blocs/complex_bloc.dart deleted file mode 100644 index 1fd88f512b9..00000000000 --- a/packages/bloc_test/test/legacy/blocs/complex_bloc.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -abstract class ComplexEvent {} - -class ComplexEventA extends ComplexEvent {} - -class ComplexEventB extends ComplexEvent {} - -abstract class ComplexState {} - -class ComplexStateA extends ComplexState {} - -class ComplexStateB extends ComplexState {} - -class ComplexBloc extends Bloc { - ComplexBloc() : super(ComplexStateA()); - - @override - Stream mapEventToState(ComplexEvent event) async* { - if (event is ComplexEventA) { - yield ComplexStateA(); - } else if (event is ComplexEventB) { - yield ComplexStateB(); - } - } -} diff --git a/packages/bloc_test/test/legacy/blocs/counter_bloc.dart b/packages/bloc_test/test/legacy/blocs/counter_bloc.dart deleted file mode 100644 index 9b683df95b6..00000000000 --- a/packages/bloc_test/test/legacy/blocs/counter_bloc.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -enum CounterEvent { increment } - -class CounterBloc extends Bloc { - CounterBloc() : super(0); - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.increment: - yield state + 1; - break; - } - } -} diff --git a/packages/bloc_test/test/legacy/blocs/debounce_counter_bloc.dart b/packages/bloc_test/test/legacy/blocs/debounce_counter_bloc.dart deleted file mode 100644 index f360f2cd405..00000000000 --- a/packages/bloc_test/test/legacy/blocs/debounce_counter_bloc.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:rxdart/rxdart.dart'; - -import 'blocs.dart'; - -class DebounceCounterBloc extends Bloc { - DebounceCounterBloc() : super(0); - - @override - Stream> transformEvents( - Stream events, - // ignore: deprecated_member_use - TransitionFunction transitionFn, - ) { - return events - .debounceTime(const Duration(milliseconds: 300)) - .switchMap(transitionFn); - } - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.increment: - yield state + 1; - break; - } - } -} diff --git a/packages/bloc_test/test/legacy/blocs/error_counter_bloc.dart b/packages/bloc_test/test/legacy/blocs/error_counter_bloc.dart deleted file mode 100644 index b8445c20315..00000000000 --- a/packages/bloc_test/test/legacy/blocs/error_counter_bloc.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import 'blocs.dart'; - -class ErrorCounterBlocError extends Error {} - -class ErrorCounterBloc extends Bloc { - ErrorCounterBloc() : super(0); - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.increment: - yield state + 1; - throw ErrorCounterBlocError(); - } - } -} diff --git a/packages/bloc_test/test/legacy/blocs/exception_counter_bloc.dart b/packages/bloc_test/test/legacy/blocs/exception_counter_bloc.dart deleted file mode 100644 index f91988a187a..00000000000 --- a/packages/bloc_test/test/legacy/blocs/exception_counter_bloc.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import 'blocs.dart'; - -class ExceptionCounterBlocException implements Exception {} - -class ExceptionCounterBloc extends Bloc { - ExceptionCounterBloc() : super(0); - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.increment: - yield state + 1; - throw ExceptionCounterBlocException(); - } - } -} diff --git a/packages/bloc_test/test/legacy/blocs/instant_emit_bloc.dart b/packages/bloc_test/test/legacy/blocs/instant_emit_bloc.dart deleted file mode 100644 index 699cd7d0226..00000000000 --- a/packages/bloc_test/test/legacy/blocs/instant_emit_bloc.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:bloc/bloc.dart'; - -import 'blocs.dart'; - -class InstantEmitBloc extends Bloc { - InstantEmitBloc() : super(0) { - add(CounterEvent.increment); - } - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.increment: - yield state + 1; - break; - } - } -} diff --git a/packages/bloc_test/test/legacy/blocs/multi_counter_bloc.dart b/packages/bloc_test/test/legacy/blocs/multi_counter_bloc.dart deleted file mode 100644 index 845f4783843..00000000000 --- a/packages/bloc_test/test/legacy/blocs/multi_counter_bloc.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import 'blocs.dart'; - -class MultiCounterBloc extends Bloc { - MultiCounterBloc() : super(0); - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.increment: - yield state + 1; - yield state + 1; - break; - } - } -} diff --git a/packages/bloc_test/test/legacy/blocs/side_effect_counter_bloc.dart b/packages/bloc_test/test/legacy/blocs/side_effect_counter_bloc.dart deleted file mode 100644 index 31b0f34413a..00000000000 --- a/packages/bloc_test/test/legacy/blocs/side_effect_counter_bloc.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import 'blocs.dart'; - -class Repository { - void sideEffect() {} -} - -class SideEffectCounterBloc extends Bloc { - SideEffectCounterBloc(this.repository) : super(0); - - final Repository repository; - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.increment: - repository.sideEffect(); - yield state + 1; - break; - } - } -} diff --git a/packages/bloc_test/test/legacy/blocs/sum_bloc.dart b/packages/bloc_test/test/legacy/blocs/sum_bloc.dart deleted file mode 100644 index 8f6a5c593af..00000000000 --- a/packages/bloc_test/test/legacy/blocs/sum_bloc.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import 'counter_bloc.dart'; - -class SumEvent { - const SumEvent(this.value); - - final int value; -} - -class SumBloc extends Bloc { - SumBloc(CounterBloc counterBloc) : super(0) { - _countSubscription = counterBloc.stream.listen( - (count) => add(SumEvent(count)), - ); - } - - late StreamSubscription _countSubscription; - - @override - Stream mapEventToState( - SumEvent event, - ) async* { - yield state + event.value; - } - - @override - Future close() { - _countSubscription.cancel(); - return super.close(); - } -} diff --git a/packages/bloc_test/test/legacy/cubits/async_counter_cubit.dart b/packages/bloc_test/test/legacy/cubits/async_counter_cubit.dart deleted file mode 100644 index 5a52ed2bb71..00000000000 --- a/packages/bloc_test/test/legacy/cubits/async_counter_cubit.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -class AsyncCounterCubit extends Cubit { - AsyncCounterCubit() : super(0); - - Future increment() async { - await Future.delayed(const Duration(microseconds: 1)); - emit(state + 1); - } -} diff --git a/packages/bloc_test/test/legacy/cubits/complex_cubit.dart b/packages/bloc_test/test/legacy/cubits/complex_cubit.dart deleted file mode 100644 index 26638f9434c..00000000000 --- a/packages/bloc_test/test/legacy/cubits/complex_cubit.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:bloc/bloc.dart'; - -abstract class ComplexState {} - -class ComplexStateA extends ComplexState {} - -class ComplexStateB extends ComplexState {} - -class ComplexCubit extends Cubit { - ComplexCubit() : super(ComplexStateA()); - - void emitA() => emit(ComplexStateA()); - void emitB() => emit(ComplexStateB()); -} diff --git a/packages/bloc_test/test/legacy/cubits/counter_cubit.dart b/packages/bloc_test/test/legacy/cubits/counter_cubit.dart deleted file mode 100644 index a58b3cd745b..00000000000 --- a/packages/bloc_test/test/legacy/cubits/counter_cubit.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:bloc/bloc.dart'; - -class CounterCubit extends Cubit { - CounterCubit() : super(0); - - void increment() => emit(state + 1); -} diff --git a/packages/bloc_test/test/legacy/cubits/cubits.dart b/packages/bloc_test/test/legacy/cubits/cubits.dart deleted file mode 100644 index d1f7408bb1a..00000000000 --- a/packages/bloc_test/test/legacy/cubits/cubits.dart +++ /dev/null @@ -1,10 +0,0 @@ -export 'async_counter_cubit.dart'; -export 'complex_cubit.dart'; -export 'counter_cubit.dart'; -export 'delayed_counter_cubit.dart'; -export 'error_cubit.dart'; -export 'exception_cubit.dart'; -export 'instant_emit_cubit.dart'; -export 'multi_counter_cubit.dart'; -export 'side_effect_counter_cubit.dart'; -export 'sum_cubit.dart'; diff --git a/packages/bloc_test/test/legacy/cubits/delayed_counter_cubit.dart b/packages/bloc_test/test/legacy/cubits/delayed_counter_cubit.dart deleted file mode 100644 index 14f581f940e..00000000000 --- a/packages/bloc_test/test/legacy/cubits/delayed_counter_cubit.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:bloc/bloc.dart'; - -class DelayedCounterCubit extends Cubit { - DelayedCounterCubit() : super(0); - - void increment() { - Future.delayed( - const Duration(milliseconds: 300), - () => emit(state + 1), - ); - } -} diff --git a/packages/bloc_test/test/legacy/cubits/error_cubit.dart b/packages/bloc_test/test/legacy/cubits/error_cubit.dart deleted file mode 100644 index a7f913fbc62..00000000000 --- a/packages/bloc_test/test/legacy/cubits/error_cubit.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:bloc/bloc.dart'; - -class ErrorCubit extends Cubit { - ErrorCubit() : super(0); - - void throwError(Error e) => throw e; -} diff --git a/packages/bloc_test/test/legacy/cubits/exception_cubit.dart b/packages/bloc_test/test/legacy/cubits/exception_cubit.dart deleted file mode 100644 index 28f2cce3361..00000000000 --- a/packages/bloc_test/test/legacy/cubits/exception_cubit.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:bloc/bloc.dart'; - -class ExceptionCubit extends Cubit { - ExceptionCubit() : super(0); - - void throwException(Exception e) => throw e; -} diff --git a/packages/bloc_test/test/legacy/cubits/instant_emit_cubit.dart b/packages/bloc_test/test/legacy/cubits/instant_emit_cubit.dart deleted file mode 100644 index f93a6681a3d..00000000000 --- a/packages/bloc_test/test/legacy/cubits/instant_emit_cubit.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:bloc/bloc.dart'; - -class InstantEmitCubit extends Cubit { - InstantEmitCubit() : super(0) { - emit(1); - } - - void increment() => emit(state + 1); -} diff --git a/packages/bloc_test/test/legacy/cubits/multi_counter_cubit.dart b/packages/bloc_test/test/legacy/cubits/multi_counter_cubit.dart deleted file mode 100644 index 6a3dfc6cad4..00000000000 --- a/packages/bloc_test/test/legacy/cubits/multi_counter_cubit.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:bloc/bloc.dart'; - -class MultiCounterCubit extends Cubit { - MultiCounterCubit() : super(0); - - void increment() { - emit(state + 1); - emit(state + 1); - } -} diff --git a/packages/bloc_test/test/legacy/cubits/side_effect_counter_cubit.dart b/packages/bloc_test/test/legacy/cubits/side_effect_counter_cubit.dart deleted file mode 100644 index 4b320fd2d18..00000000000 --- a/packages/bloc_test/test/legacy/cubits/side_effect_counter_cubit.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:bloc/bloc.dart'; - -class Repository { - void sideEffect() {} -} - -class SideEffectCounterCubit extends Cubit { - SideEffectCounterCubit(this.repository) : super(0); - - final Repository repository; - - void increment() { - repository.sideEffect(); - emit(state + 1); - } -} diff --git a/packages/bloc_test/test/legacy/cubits/sum_cubit.dart b/packages/bloc_test/test/legacy/cubits/sum_cubit.dart deleted file mode 100644 index ac307cd783b..00000000000 --- a/packages/bloc_test/test/legacy/cubits/sum_cubit.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; - -import 'counter_cubit.dart'; - -class SumCubit extends Cubit { - SumCubit(CounterCubit counterCubit) : super(0) { - _countSubscription = counterCubit.stream.listen( - (count) => emit(state + count), - ); - } - - late StreamSubscription _countSubscription; - - @override - Future close() { - _countSubscription.cancel(); - return super.close(); - } -} diff --git a/packages/bloc_test/test/legacy/mock_bloc_test.dart b/packages/bloc_test/test/legacy/mock_bloc_test.dart deleted file mode 100644 index 5697a737256..00000000000 --- a/packages/bloc_test/test/legacy/mock_bloc_test.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:bloc_test/bloc_test.dart'; -import 'package:mocktail/mocktail.dart'; - -import 'package:test/test.dart'; - -import 'blocs/blocs.dart'; -import 'cubits/cubits.dart'; - -class MockCounterBloc extends MockBloc - implements CounterBloc {} - -class MockCounterCubit extends MockCubit implements CounterCubit {} - -void main() { - group('MockBloc', () { - late CounterBloc counterBloc; - - setUpAll(() { - registerFallbackValue(CounterEvent.increment); - }); - - setUp(() { - counterBloc = MockCounterBloc(); - }); - - test('is compatible with when', () { - when(() => counterBloc.state).thenReturn(10); - expect(counterBloc.state, 10); - }); - - test('is compatible with listen', () { - // ignore: deprecated_member_use - expect(counterBloc.listen((_) {}), isA()); - expect(counterBloc.stream.listen((_) {}), isA()); - }); - - test('is compatible with emit', () { - counterBloc.emit(10); - }); - - test('is compatible with add', () { - counterBloc.add(CounterEvent.increment); - }); - - test('is compatible with addError without StackTrace', () { - counterBloc.addError(Exception('oops')); - }); - - test('is compatible with addError with StackTrace', () { - counterBloc.addError(Exception('oops'), StackTrace.empty); - }); - - test('is compatible with onEvent', () { - // ignore: invalid_use_of_protected_member - counterBloc.onEvent(CounterEvent.increment); - }); - - test('is compatible with onError', () { - // ignore: invalid_use_of_protected_member - counterBloc.onError(Exception('oops'), StackTrace.empty); - }); - - test('is compatible with onTransition', () { - // ignore: invalid_use_of_protected_member - counterBloc.onTransition( - const Transition( - currentState: 0, - event: CounterEvent.increment, - nextState: 1, - ), - ); - }); - - test('is compatible with close', () { - expect(counterBloc.close(), completes); - }); - - test('is compatible with mapEventToState', () { - expect( - counterBloc.mapEventToState(CounterEvent.increment), - isA>(), - ); - }); - - test('is automatically compatible with whenListen', () { - whenListen( - counterBloc, - Stream.fromIterable([0, 1, 2, 3]), - ); - expectLater( - counterBloc.stream, - emitsInOrder( - [equals(0), equals(1), equals(2), equals(3), emitsDone], - ), - ); - }); - - test('is automatically compatible with whenListen (legacy)', () { - final states = []; - whenListen( - counterBloc, - Stream.fromIterable([0, 1, 2, 3]), - ); - // ignore: deprecated_member_use - counterBloc.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - expect(counterBloc.state, equals(3)); - }); - }); - }); - - group('MockCubit', () { - late CounterCubit counterCubit; - - setUp(() { - counterCubit = MockCounterCubit(); - }); - - test('is compatible with when', () { - when(() => counterCubit.state).thenReturn(10); - expect(counterCubit.state, 10); - }); - - test('is automatically compatible with whenListen', () { - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - expectLater( - counterCubit.stream, - emitsInOrder( - [equals(0), equals(1), equals(2), equals(3), emitsDone], - ), - ); - }); - - test('is automatically compatible with whenListen (legacy)', () { - final states = []; - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }); - }); - }); -} diff --git a/packages/bloc_test/test/legacy/when_listen_test.dart b/packages/bloc_test/test/legacy/when_listen_test.dart deleted file mode 100644 index 390fdead93b..00000000000 --- a/packages/bloc_test/test/legacy/when_listen_test.dart +++ /dev/null @@ -1,314 +0,0 @@ -import 'dart:async'; - -import 'package:bloc_test/bloc_test.dart'; -import 'package:pedantic/pedantic.dart'; -import 'package:test/test.dart'; - -import 'cubits/cubits.dart'; - -class MockCounterCubit extends MockCubit implements CounterCubit {} - -void main() { - group('whenListen', () { - test('can mock the stream of a single cubit with an empty Stream', () { - final counterCubit = MockCounterCubit(); - whenListen(counterCubit, const Stream.empty()); - expectLater(counterCubit.stream, emitsInOrder([])); - }); - - test('can mock the stream of a single cubit', () async { - final counterCubit = MockCounterCubit(); - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - await expectLater( - counterCubit.stream, - emitsInOrder( - [equals(0), equals(1), equals(2), equals(3), emitsDone], - ), - ); - }); - - test('can mock the stream of a single cubit with delays', () async { - final counterCubit = MockCounterCubit(); - final controller = StreamController(); - whenListen(counterCubit, controller.stream); - unawaited(expectLater( - counterCubit.stream, - emitsInOrder( - [equals(0), equals(1), equals(2), equals(3), emitsDone], - ), - )); - controller.add(0); - await Future.delayed(Duration.zero); - controller.add(1); - await Future.delayed(Duration.zero); - controller.add(2); - await Future.delayed(Duration.zero); - controller.add(3); - await controller.close(); - }); - - test('can mock the state of a single cubit with delays', () async { - final counterCubit = MockCounterCubit(); - final controller = StreamController(); - whenListen(counterCubit, controller.stream); - unawaited(expectLater( - counterCubit.stream, - emitsInOrder( - [equals(0), equals(1), equals(2), equals(3), emitsDone], - ), - ).then((dynamic _) { - expect(counterCubit.state, equals(3)); - })); - controller.add(0); - await Future.delayed(Duration.zero); - controller.add(1); - await Future.delayed(Duration.zero); - controller.add(2); - await Future.delayed(Duration.zero); - controller.add(3); - await controller.close(); - }); - - test('can mock the state of a single cubit', () async { - final counterCubit = MockCounterCubit(); - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - await expectLater( - counterCubit.stream, - emitsInOrder( - [equals(0), equals(1), equals(2), equals(3), emitsDone], - ), - ); - expect(counterCubit.state, equals(3)); - }); - - test('can mock the initial state of a single cubit', () async { - final counterCubit = MockCounterCubit(); - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - initialState: 0, - ); - expect(counterCubit.state, equals(0)); - await expectLater( - counterCubit.stream, - emitsInOrder( - [equals(0), equals(1), equals(2), equals(3), emitsDone], - ), - ); - expect(counterCubit.state, equals(3)); - }); - - test('can mock the stream of a single cubit as broadcast stream', () { - final counterCubit = MockCounterCubit(); - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - expectLater( - counterCubit.stream, - emitsInOrder( - [equals(0), equals(1), equals(2), equals(3), emitsDone], - ), - ); - expectLater( - counterCubit.stream, - emitsInOrder( - [equals(0), equals(1), equals(2), equals(3), emitsDone], - ), - ); - }); - - test( - 'can mock the stream of a cubit dependency ' - '(with initial state)', () async { - final controller = StreamController(); - final counterCubit = MockCounterCubit(); - whenListen(counterCubit, controller.stream); - final sumCubit = SumCubit(counterCubit); - unawaited(expectLater(sumCubit.stream, emitsInOrder([0, 1, 3, 6]))); - controller - ..add(0) - ..add(1) - ..add(2) - ..add(3); - await controller.close(); - expect(sumCubit.state, equals(6)); - }); - - test('can mock the stream of a cubit dependency', () async { - final controller = StreamController(); - final counterCubit = MockCounterCubit(); - whenListen(counterCubit, controller.stream); - final sumCubit = SumCubit(counterCubit); - unawaited(expectLater(sumCubit.stream, emitsInOrder([1, 3, 6]))); - controller - ..add(1) - ..add(2) - ..add(3); - await controller.close(); - expect(sumCubit.state, equals(6)); - }); - }); - - group('whenListen (legacy)', () { - test('can mock the stream of a single cubit with an empty Stream', () { - final counterCubit = MockCounterCubit(); - final states = []; - whenListen(counterCubit, const Stream.empty()); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, isEmpty); - }); - }); - - test('can mock the stream of a single cubit', () { - final counterCubit = MockCounterCubit(); - final states = []; - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - }); - }); - - test('can mock the stream of a single cubit with delays', () async { - final counterCubit = MockCounterCubit(); - final controller = StreamController(); - final states = []; - whenListen(counterCubit, controller.stream); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - }); - controller.add(0); - await Future.delayed(Duration.zero); - controller.add(1); - await Future.delayed(Duration.zero); - controller.add(2); - await Future.delayed(Duration.zero); - controller.add(3); - await controller.close(); - }); - - test('can mock the state of a single cubit with delays', () async { - final counterCubit = MockCounterCubit(); - final controller = StreamController(); - final states = []; - whenListen(counterCubit, controller.stream); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }); - controller.add(0); - await Future.delayed(Duration.zero); - controller.add(1); - await Future.delayed(Duration.zero); - controller.add(2); - await Future.delayed(Duration.zero); - controller.add(3); - await controller.close(); - }); - - test('can mock the state of a single cubit', () async { - final counterCubit = MockCounterCubit(); - final states = []; - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }); - }); - - test('can mock the initial state of a single cubit', () async { - final counterCubit = MockCounterCubit(); - final states = []; - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - initialState: 0, - ); - expect(counterCubit.state, equals(0)); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }); - }); - - test('can mock the stream of a single cubit as broadcast stream', () { - final counterCubit = MockCounterCubit(); - final statesA = []; - final statesB = []; - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - counterCubit - // ignore: deprecated_member_use - ..listen(statesA.add, onDone: () { - expect(statesA, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }) - // ignore: deprecated_member_use - ..listen(statesB.add, onDone: () { - expect(statesB, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }); - }); - - test( - 'can mock the stream of a cubit dependency ' - '(with initial state)', () async { - final controller = StreamController(); - final counterCubit = MockCounterCubit(); - final states = []; - whenListen(counterCubit, controller.stream); - final sumCubit = SumCubit(counterCubit); - // ignore: deprecated_member_use - sumCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 3, 6])); - expect(sumCubit.state, equals(6)); - }); - controller - ..add(0) - ..add(1) - ..add(2) - ..add(3); - await controller.close(); - expect(sumCubit.state, equals(6)); - }); - - test('can mock the stream of a cubit dependency', () async { - final controller = StreamController(); - final counterCubit = MockCounterCubit(); - final states = []; - whenListen(counterCubit, controller.stream); - final sumCubit = SumCubit(counterCubit); - // ignore: deprecated_member_use - sumCubit.listen(states.add, onDone: () { - expect(states, equals([1, 3, 6])); - expect(sumCubit.state, equals(6)); - }); - controller - ..add(1) - ..add(2) - ..add(3); - await controller.close(); - expect(sumCubit.state, equals(6)); - }); - }); -} diff --git a/packages/bloc_test/test/mock_bloc_test.dart b/packages/bloc_test/test/mock_bloc_test.dart index 0aeca8cb454..ed3e5c4ee1b 100644 --- a/packages/bloc_test/test/mock_bloc_test.dart +++ b/packages/bloc_test/test/mock_bloc_test.dart @@ -18,10 +18,6 @@ void main() { group('MockBloc', () { late CounterBloc counterBloc; - setUpAll(() { - registerFallbackValue(CounterEvent.increment); - }); - setUp(() { counterBloc = MockCounterBloc(); }); @@ -32,15 +28,9 @@ void main() { }); test('is compatible with listen', () { - // ignore: deprecated_member_use - expect(counterBloc.listen((_) {}), isA()); expect(counterBloc.stream.listen((_) {}), isA()); }); - test('is compatible with emit', () { - counterBloc.emit(10); - }); - test('is compatible with add', () { counterBloc.add(CounterEvent.increment); }); @@ -78,14 +68,6 @@ void main() { expect(counterBloc.close(), completes); }); - test('is compatible with mapEventToState', () { - expect( - // ignore: deprecated_member_use - counterBloc.mapEventToState(CounterEvent.increment), - isA>(), - ); - }); - test('is automatically compatible with whenListen', () { whenListen( counterBloc, @@ -98,19 +80,6 @@ void main() { ), ); }); - - test('is automatically compatible with whenListen (legacy)', () { - final states = []; - whenListen( - counterBloc, - Stream.fromIterable([0, 1, 2, 3]), - ); - // ignore: deprecated_member_use - counterBloc.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - expect(counterBloc.state, equals(3)); - }); - }); }); group('MockCubit', () { @@ -137,18 +106,5 @@ void main() { ), ); }); - - test('is automatically compatible with whenListen (legacy)', () { - final states = []; - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }); - }); }); } diff --git a/packages/bloc_test/test/when_listen_test.dart b/packages/bloc_test/test/when_listen_test.dart index 390fdead93b..1127ddd9d7b 100644 --- a/packages/bloc_test/test/when_listen_test.dart +++ b/packages/bloc_test/test/when_listen_test.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; -import 'package:pedantic/pedantic.dart'; import 'package:test/test.dart'; import 'cubits/cubits.dart'; class MockCounterCubit extends MockCubit implements CounterCubit {} +void unawaited(Future? _) {} + void main() { group('whenListen', () { test('can mock the stream of a single cubit with an empty Stream', () { @@ -155,160 +156,4 @@ void main() { expect(sumCubit.state, equals(6)); }); }); - - group('whenListen (legacy)', () { - test('can mock the stream of a single cubit with an empty Stream', () { - final counterCubit = MockCounterCubit(); - final states = []; - whenListen(counterCubit, const Stream.empty()); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, isEmpty); - }); - }); - - test('can mock the stream of a single cubit', () { - final counterCubit = MockCounterCubit(); - final states = []; - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - }); - }); - - test('can mock the stream of a single cubit with delays', () async { - final counterCubit = MockCounterCubit(); - final controller = StreamController(); - final states = []; - whenListen(counterCubit, controller.stream); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - }); - controller.add(0); - await Future.delayed(Duration.zero); - controller.add(1); - await Future.delayed(Duration.zero); - controller.add(2); - await Future.delayed(Duration.zero); - controller.add(3); - await controller.close(); - }); - - test('can mock the state of a single cubit with delays', () async { - final counterCubit = MockCounterCubit(); - final controller = StreamController(); - final states = []; - whenListen(counterCubit, controller.stream); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }); - controller.add(0); - await Future.delayed(Duration.zero); - controller.add(1); - await Future.delayed(Duration.zero); - controller.add(2); - await Future.delayed(Duration.zero); - controller.add(3); - await controller.close(); - }); - - test('can mock the state of a single cubit', () async { - final counterCubit = MockCounterCubit(); - final states = []; - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }); - }); - - test('can mock the initial state of a single cubit', () async { - final counterCubit = MockCounterCubit(); - final states = []; - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - initialState: 0, - ); - expect(counterCubit.state, equals(0)); - // ignore: deprecated_member_use - counterCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }); - }); - - test('can mock the stream of a single cubit as broadcast stream', () { - final counterCubit = MockCounterCubit(); - final statesA = []; - final statesB = []; - whenListen( - counterCubit, - Stream.fromIterable([0, 1, 2, 3]), - ); - counterCubit - // ignore: deprecated_member_use - ..listen(statesA.add, onDone: () { - expect(statesA, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }) - // ignore: deprecated_member_use - ..listen(statesB.add, onDone: () { - expect(statesB, equals([0, 1, 2, 3])); - expect(counterCubit.state, equals(3)); - }); - }); - - test( - 'can mock the stream of a cubit dependency ' - '(with initial state)', () async { - final controller = StreamController(); - final counterCubit = MockCounterCubit(); - final states = []; - whenListen(counterCubit, controller.stream); - final sumCubit = SumCubit(counterCubit); - // ignore: deprecated_member_use - sumCubit.listen(states.add, onDone: () { - expect(states, equals([0, 1, 3, 6])); - expect(sumCubit.state, equals(6)); - }); - controller - ..add(0) - ..add(1) - ..add(2) - ..add(3); - await controller.close(); - expect(sumCubit.state, equals(6)); - }); - - test('can mock the stream of a cubit dependency', () async { - final controller = StreamController(); - final counterCubit = MockCounterCubit(); - final states = []; - whenListen(counterCubit, controller.stream); - final sumCubit = SumCubit(counterCubit); - // ignore: deprecated_member_use - sumCubit.listen(states.add, onDone: () { - expect(states, equals([1, 3, 6])); - expect(sumCubit.state, equals(6)); - }); - controller - ..add(1) - ..add(2) - ..add(3); - await controller.close(); - expect(sumCubit.state, equals(6)); - }); - }); } diff --git a/packages/flutter_bloc/CHANGELOG.md b/packages/flutter_bloc/CHANGELOG.md index 5c0036cf029..a0fbbf41043 100644 --- a/packages/flutter_bloc/CHANGELOG.md +++ b/packages/flutter_bloc/CHANGELOG.md @@ -1,3 +1,15 @@ +# 8.0.0-dev.3 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.5` + +# 8.0.0-dev.2 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.3` + +# 8.0.0-dev.1 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.2` + # 7.3.3 - fix: add missing child assertion to `BlocListener` and `BlocProvider` ([#2924](https://github.com/felangel/bloc/pull/2924)) diff --git a/packages/flutter_bloc/example/lib/main.dart b/packages/flutter_bloc/example/lib/main.dart index 248b16bead0..84a002bef49 100644 --- a/packages/flutter_bloc/example/lib/main.dart +++ b/packages/flutter_bloc/example/lib/main.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; void main() { - Bloc.observer = AppBlocObserver(); - runApp(const App()); + BlocOverrides.runZoned( + () => runApp(const App()), + blocObserver: AppBlocObserver(), + ); } /// Custom [BlocObserver] that observes all bloc and cubit state changes. diff --git a/packages/flutter_bloc/pubspec.yaml b/packages/flutter_bloc/pubspec.yaml index 0d1ef184232..d6fa469d9af 100644 --- a/packages/flutter_bloc/pubspec.yaml +++ b/packages/flutter_bloc/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_bloc description: Flutter Widgets that make it easy to implement the BLoC (Business Logic Component) design pattern. Built to be used with the bloc state management package. -version: 7.3.3 +version: 8.0.0-dev.3 repository: https://github.com/felangel/bloc/tree/master/packages/flutter_bloc issue_tracker: https://github.com/felangel/bloc/issues homepage: https://bloclibrary.dev @@ -12,7 +12,7 @@ environment: dependencies: flutter: sdk: flutter - bloc: ^7.2.0 + bloc: ^8.0.0-dev.5 provider: ^6.0.0 dev_dependencies: diff --git a/packages/flutter_bloc/test/bloc_builder_test.dart b/packages/flutter_bloc/test/bloc_builder_test.dart index c8629669fb6..779febb0f58 100644 --- a/packages/flutter_bloc/test/bloc_builder_test.dart +++ b/packages/flutter_bloc/test/bloc_builder_test.dart @@ -127,7 +127,7 @@ class MyCounterAppState extends State { } class CounterCubit extends Cubit { - CounterCubit() : super(0); + CounterCubit({int seed = 0}) : super(seed); void increment() => emit(state + 1); void decrement() => emit(state - 1); @@ -475,7 +475,7 @@ void main() { testWidgets('rebuilds when provided bloc is changed', (tester) async { final firstCounterCubit = CounterCubit(); - final secondCounterCubit = CounterCubit()..emit(100); + final secondCounterCubit = CounterCubit(seed: 100); await tester.pumpWidget( Directionality( diff --git a/packages/flutter_bloc/test/bloc_consumer_test.dart b/packages/flutter_bloc/test/bloc_consumer_test.dart index 8f8e36f3392..022e63626d5 100644 --- a/packages/flutter_bloc/test/bloc_consumer_test.dart +++ b/packages/flutter_bloc/test/bloc_consumer_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; class CounterCubit extends Cubit { - CounterCubit() : super(0); + CounterCubit({int seed = 0}) : super(seed); void increment() => emit(state + 1); } @@ -351,7 +351,7 @@ void main() { 'rebuilds and updates subscription ' 'when provided bloc is changed', (tester) async { final firstCounterCubit = CounterCubit(); - final secondCounterCubit = CounterCubit()..emit(100); + final secondCounterCubit = CounterCubit(seed: 100); final states = []; const expectedStates = [1, 101]; diff --git a/packages/flutter_bloc/test/bloc_listener_test.dart b/packages/flutter_bloc/test/bloc_listener_test.dart index 31898fae938..5d6e939b0fa 100644 --- a/packages/flutter_bloc/test/bloc_listener_test.dart +++ b/packages/flutter_bloc/test/bloc_listener_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; class CounterCubit extends Cubit { - CounterCubit() : super(0); + CounterCubit({int seed = 0}) : super(seed); void increment() => emit(state + 1); } @@ -466,7 +466,7 @@ void main() { 'updates subscription ' 'when provided bloc is changed', (tester) async { final firstCounterCubit = CounterCubit(); - final secondCounterCubit = CounterCubit()..emit(100); + final secondCounterCubit = CounterCubit(seed: 100); final states = []; const expectedStates = [1, 101]; diff --git a/packages/flutter_bloc/test/bloc_provider_test.dart b/packages/flutter_bloc/test/bloc_provider_test.dart index 0b4b34d8780..3e3a5b23b8c 100644 --- a/packages/flutter_bloc/test/bloc_provider_test.dart +++ b/packages/flutter_bloc/test/bloc_provider_test.dart @@ -8,19 +8,7 @@ class MockCubit extends Cubit { MockCubit(S state) : super(state); @override - StreamSubscription listen( - void Function(S p1)? onData, { - Function? onError, - void Function()? onDone, - bool? cancelOnError, - }) { - return Stream.empty().listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ); - } + Stream get stream => Stream.empty(); } class MyApp extends StatelessWidget { diff --git a/packages/flutter_bloc/test/bloc_selector_test.dart b/packages/flutter_bloc/test/bloc_selector_test.dart index df669e2daf2..4ef22ef314d 100644 --- a/packages/flutter_bloc/test/bloc_selector_test.dart +++ b/packages/flutter_bloc/test/bloc_selector_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; class CounterCubit extends Cubit { - CounterCubit() : super(0); + CounterCubit({int seed = 0}) : super(seed); void increment() => emit(state + 1); } @@ -95,7 +95,7 @@ void main() { testWidgets('rebuilds when provided bloc is changed', (tester) async { final firstCounterCubit = CounterCubit(); - final secondCounterCubit = CounterCubit()..emit(100); + final secondCounterCubit = CounterCubit(seed: 100); await tester.pumpWidget( Directionality( @@ -142,7 +142,7 @@ void main() { testWidgets('rebuilds when bloc is changed at runtime', (tester) async { final firstCounterCubit = CounterCubit(); - final secondCounterCubit = CounterCubit()..emit(100); + final secondCounterCubit = CounterCubit(seed: 100); await tester.pumpWidget( Directionality( diff --git a/packages/hydrated_bloc/CHANGELOG.md b/packages/hydrated_bloc/CHANGELOG.md index 9680cc301e2..628d66f67e8 100644 --- a/packages/hydrated_bloc/CHANGELOG.md +++ b/packages/hydrated_bloc/CHANGELOG.md @@ -1,3 +1,24 @@ +# 8.0.0-dev.5 + +- **BREAKING**: feat: introduce `HydratedBlocOverrides` API ([#2947](https://github.com/felangel/bloc/pull/2947)) + - `HydratedBloc.storage` removed in favor of `HydratedBlocOverrides.runZoned` and `HydratedBlocOverrides.current.storage` + +# 8.0.0-dev.4 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.5` + +# 8.0.0-dev.3 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.4` + +# 8.0.0-dev.2 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.3` + +# 8.0.0-dev.1 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.2` + # 7.1.0 - feat: upgrade to `bloc ^7.2.0` diff --git a/packages/hydrated_bloc/README.md b/packages/hydrated_bloc/README.md index 5e7b6e617f0..e02c4bee96e 100644 --- a/packages/hydrated_bloc/README.md +++ b/packages/hydrated_bloc/README.md @@ -4,7 +4,7 @@

build - codecov + codecov Star on Github style: effective dart Flutter Website @@ -53,10 +53,13 @@ Our top sponsors are shown below! [[Become a Sponsor](https://github.com/sponsor ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); - HydratedBloc.storage = await HydratedStorage.build( + final storage = await HydratedStorage.build( storageDirectory: await getTemporaryDirectory(), ); - runApp(App()); + HydratedBlocOverrides.runZoned( + () => runApp(App()), + storage: storage, + ); } ``` @@ -85,7 +88,7 @@ class Increment extends CounterEvent {} class CounterBloc extends HydratedBloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); - } + } @override int fromJson(Map json) => json['value'] as int; @@ -120,7 +123,7 @@ class CounterCubit extends Cubit with HydratedMixin { Any `storageDirectory` can be used when creating an instance of `HydratedStorage`: ```dart -HydratedBloc.storage = await HydratedStorage.build( +final storage = await HydratedStorage.build( storageDirectory: await getApplicationDocumentsDirectory(), ); ``` @@ -158,7 +161,10 @@ class MyHydratedStorage implements Storage { ```dart // main.dart -HydratedBloc.storage = MyHydratedStorage(); +HydratedBlocOverrides.runZoned( + () => runApp(MyApp()), + storage: MyHydratedStorage(), +); ``` ## Dart Versions diff --git a/packages/hydrated_bloc/build.yaml b/packages/hydrated_bloc/build.yaml new file mode 100644 index 00000000000..cb0a108faab --- /dev/null +++ b/packages/hydrated_bloc/build.yaml @@ -0,0 +1,7 @@ +targets: + $default: + builders: + source_gen|combining_builder: + options: + ignore_for_file: + - implicit_dynamic_parameter \ No newline at end of file diff --git a/packages/hydrated_bloc/example/lib/main.dart b/packages/hydrated_bloc/example/lib/main.dart index 5650044a3c5..6424752eaf8 100644 --- a/packages/hydrated_bloc/example/lib/main.dart +++ b/packages/hydrated_bloc/example/lib/main.dart @@ -7,12 +7,15 @@ import 'package:path_provider/path_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - HydratedBloc.storage = await HydratedStorage.build( + final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); - runApp(App()); + HydratedBlocOverrides.runZoned( + () => runApp(App()), + storage: storage, + ); } class App extends StatelessWidget { @@ -66,38 +69,27 @@ class CounterView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end, children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), - child: FloatingActionButton( - child: const Icon(Icons.brightness_6), - onPressed: () { - context.read().toggleBrightness(); - }, - ), + FloatingActionButton( + child: const Icon(Icons.brightness_6), + onPressed: () => context.read().toggleBrightness(), ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), - child: FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () => context.read().add(Increment()), - ), + const SizedBox(height: 4), + FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () => context.read().add(Increment()), ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), - child: FloatingActionButton( - child: const Icon(Icons.remove), - onPressed: () => context.read().add(Decrement()), - ), + const SizedBox(height: 4), + FloatingActionButton( + child: const Icon(Icons.remove), + onPressed: () => context.read().add(Decrement()), ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), - child: FloatingActionButton( - child: const Icon(Icons.delete_forever), - onPressed: () async { - await HydratedBloc.storage.clear(); - context.read().add(Reset()); - }, - ), + const SizedBox(height: 4), + FloatingActionButton( + child: const Icon(Icons.delete_forever), + onPressed: () { + context.read().clear(); + context.read().clear(); + }, ), ], ), @@ -111,13 +103,10 @@ class Increment extends CounterEvent {} class Decrement extends CounterEvent {} -class Reset extends CounterEvent {} - class CounterBloc extends HydratedBloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); on((event, emit) => emit(state - 1)); - on((event, emit) => emit(0)); } @override diff --git a/packages/hydrated_bloc/lib/src/hydrated_bloc.dart b/packages/hydrated_bloc/lib/src/hydrated_bloc.dart index 4771647665e..0be5e50008b 100644 --- a/packages/hydrated_bloc/lib/src/hydrated_bloc.dart +++ b/packages/hydrated_bloc/lib/src/hydrated_bloc.dart @@ -1,6 +1,94 @@ +import 'dart:async'; + import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:meta/meta.dart'; +const _asyncRunZoned = runZoned; + +/// This class extends [BlocOverrides] and facilitates overriding +/// [Storage] in addition to [BlocObserver] and [EventTransformer]. +/// It should be extended by another class in client code with overrides +/// that construct a custom implementation. +/// For example: +/// +/// ```dart +/// class MyStorage extends Storage { +/// ... +/// // A custom Storage implementation. +/// ... +/// } +/// +/// void main() { +/// HydratedBlocOverrides.runZoned(() { +/// ... +/// // HydratedBloc instances will use MyStorage. +/// ... +/// }, storage: MyStorage()); +/// } +/// ``` +class HydratedBlocOverrides extends BlocOverrides { + static final _token = Object(); + + /// Returns the current [HydratedBlocOverrides] instance. + /// + /// This will return `null` if the current [Zone] does not contain + /// any [HydratedBlocOverrides]. + /// + /// See also: + /// * [HydratedBlocOverrides.runZoned] to provide [HydratedBlocOverrides] + /// in a fresh [Zone]. + /// + static HydratedBlocOverrides? get current { + return Zone.current[_token] as HydratedBlocOverrides?; + } + + /// Runs [body] in a fresh [Zone] using the provided overrides. + static R runZoned( + R Function() body, { + BlocObserver? blocObserver, + EventTransformer? eventTransformer, + Storage? storage, + }) { + final overrides = _HydratedBlocOverridesScope(storage); + return BlocOverrides.runZoned( + () => _asyncRunZoned(body, zoneValues: {_token: overrides}), + blocObserver: blocObserver, + eventTransformer: eventTransformer, + ); + } + + @override + BlocObserver get blocObserver { + return BlocOverrides.current?.blocObserver ?? super.blocObserver; + } + + @override + EventTransformer get eventTransformer { + return BlocOverrides.current?.eventTransformer ?? super.eventTransformer; + } + + /// The [Storage] that will be used within the current [Zone]. + Storage get storage => _defaultStorage; +} + +class _HydratedBlocOverridesScope extends HydratedBlocOverrides { + _HydratedBlocOverridesScope(this._storage); + + final HydratedBlocOverrides? _previous = HydratedBlocOverrides.current; + final Storage? _storage; + + @override + Storage get storage { + final storage = _storage; + if (storage != null) return storage; + + final previous = _previous; + if (previous != null) return previous.storage; + + return super.storage; + } +} + /// {@template hydrated_bloc} /// Specialized [Bloc] which handles initializing the [Bloc] state /// based on the persisted state. This allows state to be persisted @@ -32,19 +120,6 @@ abstract class HydratedBloc extends Bloc HydratedBloc(State state) : super(state) { hydrate(); } - - /// Setter for instance of [Storage] which will be used to - /// manage persisting/restoring the [Bloc] state. - static Storage? _storage; - - static set storage(Storage? storage) => _storage = storage; - - /// Instance of [Storage] which will be used to - /// manage persisting/restoring the [Bloc] state. - static Storage get storage { - if (_storage == null) throw const StorageNotFound(); - return _storage!; - } } /// {@template hydrated_cubit} @@ -100,6 +175,15 @@ abstract class HydratedCubit extends Cubit /// * [HydratedCubit] to enable automatic state persistence/restoration with [Cubit] /// mixin HydratedMixin on BlocBase { + late final _overrides = HydratedBlocOverrides.current; + + Storage get _storage { + final storage = _overrides?.storage; + if (storage == null) throw const StorageNotFound(); + if (storage is _DefaultStorage) throw const StorageNotFound(); + return storage; + } + /// Populates the internal state storage with the latest state. /// This should be called when using the [HydratedMixin] /// directly within the constructor body. @@ -113,14 +197,14 @@ mixin HydratedMixin on BlocBase { /// } /// ``` void hydrate() { - final storage = HydratedBloc.storage; try { final stateJson = _toJson(state); if (stateJson != null) { - storage.write(storageToken, stateJson).then((_) {}, onError: onError); + _storage.write(storageToken, stateJson).then((_) {}, onError: onError); } } catch (error, stackTrace) { onError(error, stackTrace); + if (error is StorageNotFound) rethrow; } } @@ -128,10 +212,9 @@ mixin HydratedMixin on BlocBase { @override State get state { - final storage = HydratedBloc.storage; if (_state != null) return _state!; try { - final stateJson = storage.read(storageToken) as Map?; + final stateJson = _storage.read(storageToken) as Map?; if (stateJson == null) { _state = super.state; return super.state; @@ -153,15 +236,15 @@ mixin HydratedMixin on BlocBase { @override void onChange(Change change) { super.onChange(change); - final storage = HydratedBloc.storage; final state = change.nextState; try { final stateJson = _toJson(state); if (stateJson != null) { - storage.write(storageToken, stateJson).then((_) {}, onError: onError); + _storage.write(storageToken, stateJson).then((_) {}, onError: onError); } } catch (error, stackTrace) { onError(error, stackTrace); + rethrow; } _state = state; } @@ -309,7 +392,7 @@ mixin HydratedMixin on BlocBase { /// [clear] is used to wipe or invalidate the cache of a [HydratedBloc]. /// Calling [clear] will delete the cached state of the bloc /// but will not modify the current state of the bloc. - Future clear() => HydratedBloc.storage.delete(storageToken); + Future clear() => _storage.delete(storageToken); /// Responsible for converting the `Map` representation /// of the bloc state into a concrete instance of the bloc state. @@ -354,7 +437,11 @@ class StorageNotFound implements Exception { return 'Storage was accessed before it was initialized.\n' 'Please ensure that storage has been initialized.\n\n' 'For example:\n\n' - 'HydratedBloc.storage = await HydratedStorage.build();'; + 'final storage = await HydratedStorage.build();\n' + 'HydratedBlocOverrides.runZoned(\n' + ' () => runApp(MyApp()),\n' + ' storage: storage,\n' + ');'; } } @@ -411,3 +498,12 @@ class _Traversed { final _Outcome outcome; final dynamic value; } + +late final _defaultStorage = _DefaultStorage(); + +class _DefaultStorage implements Storage { + @override + dynamic noSuchMethod(Invocation invocation) { + return super.noSuchMethod(invocation); + } +} diff --git a/packages/hydrated_bloc/lib/src/hydrated_storage.dart b/packages/hydrated_bloc/lib/src/hydrated_storage.dart index 6f98b592b41..087c4b1ad27 100644 --- a/packages/hydrated_bloc/lib/src/hydrated_storage.dart +++ b/packages/hydrated_bloc/lib/src/hydrated_storage.dart @@ -58,12 +58,15 @@ class HydratedStorage implements Storage { /// /// void main() async { /// WidgetsFlutterBinding.ensureInitialized(); - /// HydratedBloc.storage = await HydratedStorage.build( + /// final storage = await HydratedStorage.build( /// storageDirectory: kIsWeb - /// ? HydratedStorage.webStorageDirectory - /// : await getTemporaryDirectory(), + /// ? HydratedStorage.webStorageDirectory + /// : await getTemporaryDirectory(), + /// ); + /// HydratedBlocOverrides.runZoned( + /// () => runApp(App()), + /// storage: storage, /// ); - /// runApp(App()); /// } /// ``` /// diff --git a/packages/hydrated_bloc/pubspec.yaml b/packages/hydrated_bloc/pubspec.yaml index 0780aa9eb8d..31f04c306fb 100644 --- a/packages/hydrated_bloc/pubspec.yaml +++ b/packages/hydrated_bloc/pubspec.yaml @@ -1,7 +1,7 @@ name: hydrated_bloc description: An extension to the bloc state management library which automatically persists and restores bloc states. -version: 7.1.0 -repository: https://github.com/felangel/bloc +version: 8.0.0-dev.5 +repository: https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc issue_tracker: https://github.com/felangel/bloc/issues homepage: https://bloclibrary.dev documentation: https://bloclibrary.dev/#/gettingstarted @@ -10,21 +10,20 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - bloc: ^7.2.0 + bloc: ^8.0.0-dev.5 hive: ^2.0.0 meta: ^1.3.0 synchronized: ^3.0.0 dev_dependencies: - test: ^1.16.0 - build_runner: ^1.10.0 + test: ^1.18.2 + build_runner: ^2.1.4 collection: ^1.15.0 crypto: ^3.0.0 - freezed_annotation: ^0.14.0 - freezed: ^0.14.0 - json_serializable: ^4.0.0 - json_annotation: ^4.0.0 - mocktail: ^0.1.0 + freezed_annotation: ^0.15.0 + freezed: ^0.15.0+1 + json_serializable: ^6.0.0 + json_annotation: ^4.3.0 + mocktail: ^0.2.0 path: ^1.8.0 - pedantic: ^1.10.0 uuid: ^3.0.0 diff --git a/packages/hydrated_bloc/test/cubits/freezed_cubit.dart b/packages/hydrated_bloc/test/cubits/freezed_cubit.dart index 20aad77acc0..77fdb5b1202 100644 --- a/packages/hydrated_bloc/test/cubits/freezed_cubit.dart +++ b/packages/hydrated_bloc/test/cubits/freezed_cubit.dart @@ -17,7 +17,7 @@ class FreezedCubit extends HydratedCubit { } @freezed -abstract class Question with _$Question { +class Question with _$Question { const factory Question({ int? id, String? question, @@ -28,7 +28,7 @@ abstract class Question with _$Question { } @freezed -abstract class Tree with _$Tree { +class Tree with _$Tree { const factory Tree({ Question? question, Tree? left, diff --git a/packages/hydrated_bloc/test/cubits/freezed_cubit.freezed.dart b/packages/hydrated_bloc/test/cubits/freezed_cubit.freezed.dart index 770664a17ee..a45403494e6 100644 --- a/packages/hydrated_bloc/test/cubits/freezed_cubit.freezed.dart +++ b/packages/hydrated_bloc/test/cubits/freezed_cubit.freezed.dart @@ -1,5 +1,6 @@ +// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target part of 'freezed_cubit.dart'; @@ -8,6 +9,10 @@ part of 'freezed_cubit.dart'; // ************************************************************************** T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + Question _$QuestionFromJson(Map json) { return _Question.fromJson(json); } @@ -23,7 +28,7 @@ class _$QuestionTearOff { ); } - Question fromJson(Map json) { + Question fromJson(Map json) { return Question.fromJson(json); } } @@ -33,12 +38,13 @@ const $Question = _$QuestionTearOff(); /// @nodoc mixin _$Question { - int? get id; - String? get question; + int? get id => throw _privateConstructorUsedError; + String? get question => throw _privateConstructorUsedError; - Map toJson(); + Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $QuestionCopyWith get copyWith; + $QuestionCopyWith get copyWith => + throw _privateConstructorUsedError; } /// @nodoc @@ -62,8 +68,14 @@ class _$QuestionCopyWithImpl<$Res> implements $QuestionCopyWith<$Res> { Object? question = freezed, }) { return _then(_value.copyWith( - id: id == freezed ? _value.id : id as int?, - question: question == freezed ? _value.question : question as String?, + id: id == freezed + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + question: question == freezed + ? _value.question + : question // ignore: cast_nullable_to_non_nullable + as String?, )); } } @@ -91,20 +103,25 @@ class __$QuestionCopyWithImpl<$Res> extends _$QuestionCopyWithImpl<$Res> Object? question = freezed, }) { return _then(_Question( - id: id == freezed ? _value.id : id as int?, - question: question == freezed ? _value.question : question as String?, + id: id == freezed + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int?, + question: question == freezed + ? _value.question + : question // ignore: cast_nullable_to_non_nullable + as String?, )); } } -@JsonSerializable() - /// @nodoc +@JsonSerializable() class _$_Question implements _Question { const _$_Question({this.id, this.question}); factory _$_Question.fromJson(Map json) => - _$_$_QuestionFromJson(json); + _$$_QuestionFromJson(json); @override final int? id; @@ -119,19 +136,15 @@ class _$_Question implements _Question { @override bool operator ==(dynamic other) { return identical(this, other) || - (other is _Question && - (identical(other.id, id) || - const DeepCollectionEquality().equals(other.id, id)) && + (other.runtimeType == runtimeType && + other is _Question && + (identical(other.id, id) || other.id == id) && (identical(other.question, question) || - const DeepCollectionEquality() - .equals(other.question, question))); + other.question == question)); } @override - int get hashCode => - runtimeType.hashCode ^ - const DeepCollectionEquality().hash(id) ^ - const DeepCollectionEquality().hash(question); + int get hashCode => Object.hash(runtimeType, id, question); @JsonKey(ignore: true) @override @@ -140,7 +153,7 @@ class _$_Question implements _Question { @override Map toJson() { - return _$_$_QuestionToJson(this); + return _$$_QuestionToJson(this); } } @@ -155,7 +168,8 @@ abstract class _Question implements Question { String? get question; @override @JsonKey(ignore: true) - _$QuestionCopyWith<_Question> get copyWith; + _$QuestionCopyWith<_Question> get copyWith => + throw _privateConstructorUsedError; } Tree _$TreeFromJson(Map json) { @@ -174,7 +188,7 @@ class _$TreeTearOff { ); } - Tree fromJson(Map json) { + Tree fromJson(Map json) { return Tree.fromJson(json); } } @@ -184,13 +198,13 @@ const $Tree = _$TreeTearOff(); /// @nodoc mixin _$Tree { - Question? get question; - Tree? get left; - Tree? get right; + Question? get question => throw _privateConstructorUsedError; + Tree? get left => throw _privateConstructorUsedError; + Tree? get right => throw _privateConstructorUsedError; - Map toJson(); + Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $TreeCopyWith get copyWith; + $TreeCopyWith get copyWith => throw _privateConstructorUsedError; } /// @nodoc @@ -219,9 +233,18 @@ class _$TreeCopyWithImpl<$Res> implements $TreeCopyWith<$Res> { Object? right = freezed, }) { return _then(_value.copyWith( - question: question == freezed ? _value.question : question as Question?, - left: left == freezed ? _value.left : left as Tree?, - right: right == freezed ? _value.right : right as Tree?, + question: question == freezed + ? _value.question + : question // ignore: cast_nullable_to_non_nullable + as Question?, + left: left == freezed + ? _value.left + : left // ignore: cast_nullable_to_non_nullable + as Tree?, + right: right == freezed + ? _value.right + : right // ignore: cast_nullable_to_non_nullable + as Tree?, )); } @@ -290,21 +313,29 @@ class __$QTreeCopyWithImpl<$Res> extends _$TreeCopyWithImpl<$Res> Object? right = freezed, }) { return _then(_QTree( - question: question == freezed ? _value.question : question as Question?, - left: left == freezed ? _value.left : left as Tree?, - right: right == freezed ? _value.right : right as Tree?, + question: question == freezed + ? _value.question + : question // ignore: cast_nullable_to_non_nullable + as Question?, + left: left == freezed + ? _value.left + : left // ignore: cast_nullable_to_non_nullable + as Tree?, + right: right == freezed + ? _value.right + : right // ignore: cast_nullable_to_non_nullable + as Tree?, )); } } -@JsonSerializable() - /// @nodoc +@JsonSerializable() class _$_QTree implements _QTree { const _$_QTree({this.question, this.left, this.right}); factory _$_QTree.fromJson(Map json) => - _$_$_QTreeFromJson(json); + _$$_QTreeFromJson(json); @override final Question? question; @@ -321,22 +352,16 @@ class _$_QTree implements _QTree { @override bool operator ==(dynamic other) { return identical(this, other) || - (other is _QTree && + (other.runtimeType == runtimeType && + other is _QTree && (identical(other.question, question) || - const DeepCollectionEquality() - .equals(other.question, question)) && - (identical(other.left, left) || - const DeepCollectionEquality().equals(other.left, left)) && - (identical(other.right, right) || - const DeepCollectionEquality().equals(other.right, right))); + other.question == question) && + (identical(other.left, left) || other.left == left) && + (identical(other.right, right) || other.right == right)); } @override - int get hashCode => - runtimeType.hashCode ^ - const DeepCollectionEquality().hash(question) ^ - const DeepCollectionEquality().hash(left) ^ - const DeepCollectionEquality().hash(right); + int get hashCode => Object.hash(runtimeType, question, left, right); @JsonKey(ignore: true) @override @@ -345,7 +370,7 @@ class _$_QTree implements _QTree { @override Map toJson() { - return _$_$_QTreeToJson(this); + return _$$_QTreeToJson(this); } } @@ -363,5 +388,5 @@ abstract class _QTree implements Tree { Tree? get right; @override @JsonKey(ignore: true) - _$QTreeCopyWith<_QTree> get copyWith; + _$QTreeCopyWith<_QTree> get copyWith => throw _privateConstructorUsedError; } diff --git a/packages/hydrated_bloc/test/cubits/freezed_cubit.g.dart b/packages/hydrated_bloc/test/cubits/freezed_cubit.g.dart index e2920cd4c04..0e7a6195d27 100644 --- a/packages/hydrated_bloc/test/cubits/freezed_cubit.g.dart +++ b/packages/hydrated_bloc/test/cubits/freezed_cubit.g.dart @@ -1,39 +1,37 @@ // GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: implicit_dynamic_parameter + part of 'freezed_cubit.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** -_$_Question _$_$_QuestionFromJson(Map json) { - return _$_Question( - id: json['id'] as int?, - question: json['question'] as String?, - ); -} +_$_Question _$$_QuestionFromJson(Map json) => _$_Question( + id: json['id'] as int?, + question: json['question'] as String?, + ); -Map _$_$_QuestionToJson(_$_Question instance) => +Map _$$_QuestionToJson(_$_Question instance) => { 'id': instance.id, 'question': instance.question, }; -_$_QTree _$_$_QTreeFromJson(Map json) { - return _$_QTree( - question: json['question'] == null - ? null - : Question.fromJson(json['question'] as Map), - left: json['left'] == null - ? null - : Tree.fromJson(json['left'] as Map), - right: json['right'] == null - ? null - : Tree.fromJson(json['right'] as Map), - ); -} +_$_QTree _$$_QTreeFromJson(Map json) => _$_QTree( + question: json['question'] == null + ? null + : Question.fromJson(json['question'] as Map), + left: json['left'] == null + ? null + : Tree.fromJson(json['left'] as Map), + right: json['right'] == null + ? null + : Tree.fromJson(json['right'] as Map), + ); -Map _$_$_QTreeToJson(_$_QTree instance) => { +Map _$$_QTreeToJson(_$_QTree instance) => { 'question': instance.question, 'left': instance.left, 'right': instance.right, diff --git a/packages/hydrated_bloc/test/cubits/json_serializable_cubit.g.dart b/packages/hydrated_bloc/test/cubits/json_serializable_cubit.g.dart index 3011ea66d92..a896aab4ab8 100644 --- a/packages/hydrated_bloc/test/cubits/json_serializable_cubit.g.dart +++ b/packages/hydrated_bloc/test/cubits/json_serializable_cubit.g.dart @@ -1,21 +1,21 @@ // GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: implicit_dynamic_parameter + part of 'json_serializable_cubit.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** -User _$UserFromJson(Map json) { - return User( - json['name'] as String, - json['age'] as int, - _$enumDecode(_$ColorEnumMap, json['favoriteColor']), - (json['todos'] as List) - .map((dynamic e) => Todo.fromJson(e as Map)) - .toList(), - ); -} +User _$UserFromJson(Map json) => User( + json['name'] as String, + json['age'] as int, + $enumDecode(_$ColorEnumMap, json['favoriteColor']), + (json['todos'] as List) + .map((e) => Todo.fromJson(e as Map)) + .toList(), + ); Map _$UserToJson(User instance) => { 'name': instance.name, @@ -24,44 +24,16 @@ Map _$UserToJson(User instance) => { 'todos': instance.todos.map((e) => e.toJson()).toList(), }; -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', - ); - } - - return enumValues.entries.singleWhere( - (e) => e.value == source, - orElse: () { - if (unknownValue == null) { - throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}', - ); - } - return MapEntry(unknownValue, enumValues.values.first); - }, - ).key; -} - const _$ColorEnumMap = { Color.red: 'red', Color.green: 'green', Color.blue: 'blue', }; -Todo _$TodoFromJson(Map json) { - return Todo( - json['id'] as String, - json['task'] as String, - ); -} +Todo _$TodoFromJson(Map json) => Todo( + json['id'] as String, + json['task'] as String, + ); Map _$TodoToJson(Todo instance) => { 'id': instance.id, diff --git a/packages/hydrated_bloc/test/e2e_test.dart b/packages/hydrated_bloc/test/e2e_test.dart index d27b2f6a0b3..c7350aeea6c 100644 --- a/packages/hydrated_bloc/test/e2e_test.dart +++ b/packages/hydrated_bloc/test/e2e_test.dart @@ -19,7 +19,6 @@ void main() { path.join(Directory.current.path, '.cache'), ), ); - HydratedBloc.storage = storage; }); tearDown(() async { @@ -39,316 +38,357 @@ void main() { group('FreezedCubit', () { test('persists and restores state correctly', () async { - const tree = Tree( - question: Question(id: 0, question: '?00'), - left: Tree( - question: Question(id: 1, question: '?01'), - ), - right: Tree( - question: Question(id: 2, question: '?02'), - left: Tree(question: Question(id: 3, question: '?03')), - right: Tree(question: Question(id: 4, question: '?04')), - ), - ); - final cubit = FreezedCubit(); - expect(cubit.state, isNull); - cubit.setQuestion(tree); - await sleep(); - expect(FreezedCubit().state, tree); + await HydratedBlocOverrides.runZoned(() async { + const tree = Tree( + question: Question(id: 0, question: '?00'), + left: Tree( + question: Question(id: 1, question: '?01'), + ), + right: Tree( + question: Question(id: 2, question: '?02'), + left: Tree(question: Question(id: 3, question: '?03')), + right: Tree(question: Question(id: 4, question: '?04')), + ), + ); + final cubit = FreezedCubit(); + expect(cubit.state, isNull); + cubit.setQuestion(tree); + await sleep(); + expect(FreezedCubit().state, tree); + }, storage: storage); }); }); group('JsonSerializableCubit', () { test('persists and restores state correctly', () async { - final cubit = JsonSerializableCubit(); - final expected = const User.initial().copyWith( - favoriteColor: Color.green, - ); - expect(cubit.state, const User.initial()); - cubit.updateFavoriteColor(Color.green); - await sleep(); - expect(JsonSerializableCubit().state, expected); + await HydratedBlocOverrides.runZoned(() async { + final cubit = JsonSerializableCubit(); + final expected = const User.initial().copyWith( + favoriteColor: Color.green, + ); + expect(cubit.state, const User.initial()); + cubit.updateFavoriteColor(Color.green); + await sleep(); + expect(JsonSerializableCubit().state, expected); + }, storage: storage); }); }); group('ListCubit', () { test('persists and restores string list correctly', () async { - const item = 'foo'; - final cubit = ListCubit(); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect(ListCubit().state, const [item]); + await HydratedBlocOverrides.runZoned(() async { + const item = 'foo'; + final cubit = ListCubit(); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect(ListCubit().state, const [item]); + }, storage: storage); }); test('persists and restores object->map list correctly', () async { - const item = MapObject(1); - const fromJson = MapObject.fromJson; - final cubit = ListCubitMap(fromJson); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitMap(fromJson).state, - const [item], - ); + await HydratedBlocOverrides.runZoned(() async { + const item = MapObject(1); + const fromJson = MapObject.fromJson; + final cubit = ListCubitMap(fromJson); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitMap(fromJson).state, + const [item], + ); + }, storage: storage); }); test('persists and restores object-*>map list correctly', () async { - const item = MapObject(1); - const fromJson = MapObject.fromJson; - final cubit = ListCubitMap(fromJson, true); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitMap(fromJson).state, - const [item], - ); + await HydratedBlocOverrides.runZoned(() async { + const item = MapObject(1); + const fromJson = MapObject.fromJson; + final cubit = ListCubitMap(fromJson, true); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitMap(fromJson).state, + const [item], + ); + }, storage: storage); }); test('persists and restores obj->map list correctly', () async { - final item = MapCustomObject(1); - const fromJson = MapCustomObject.fromJson; - final cubit = ListCubitMap(fromJson); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitMap(fromJson).state, - [item], - ); + await HydratedBlocOverrides.runZoned(() async { + final item = MapCustomObject(1); + const fromJson = MapCustomObject.fromJson; + final cubit = ListCubitMap(fromJson); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitMap(fromJson).state, + [item], + ); + }, storage: storage); }); test('persists and restores obj-*>map list correctly', () async { - final item = MapCustomObject(1); - const fromJson = MapCustomObject.fromJson; - final cubit = - ListCubitMap(fromJson, true); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitMap(fromJson).state, - [item], - ); + await HydratedBlocOverrides.runZoned(() async { + final item = MapCustomObject(1); + const fromJson = MapCustomObject.fromJson; + final cubit = + ListCubitMap(fromJson, true); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitMap(fromJson).state, + [item], + ); + }, storage: storage); }); test('persists and restores object->list list correctly', () async { - const item = ListObject(1); - const fromJson = ListObject.fromJson; - final cubit = ListCubitList(fromJson); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitList(fromJson).state, - const [item], - ); + await HydratedBlocOverrides.runZoned(() async { + const item = ListObject(1); + const fromJson = ListObject.fromJson; + final cubit = ListCubitList(fromJson); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitList(fromJson).state, + const [item], + ); + }, storage: storage); }); test('persists and restores object-*>list list correctly', () async { - const item = ListObject(1); - const fromJson = ListObject.fromJson; - final cubit = ListCubitList(fromJson, true); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitList(fromJson).state, - const [item], - ); + await HydratedBlocOverrides.runZoned(() async { + const item = ListObject(1); + const fromJson = ListObject.fromJson; + final cubit = ListCubitList(fromJson, true); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitList(fromJson).state, + const [item], + ); + }, storage: storage); }); test('persists and restores object->list list correctly', () async { - final item = ListMapObject(1); - const fromJson = ListMapObject.fromJson; - final cubit = ListCubitList(fromJson); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitList(fromJson).state, - [item], - ); + await HydratedBlocOverrides.runZoned(() async { + final item = ListMapObject(1); + const fromJson = ListMapObject.fromJson; + final cubit = ListCubitList(fromJson); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitList(fromJson).state, + [item], + ); + }, storage: storage); }); test('persists and restores obj-*>list list correctly', () async { - final item = ListMapObject(1); - const fromJson = ListMapObject.fromJson; - final cubit = ListCubitList(fromJson, true); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitList(fromJson).state, - [item], - ); + await HydratedBlocOverrides.runZoned(() async { + final item = ListMapObject(1); + const fromJson = ListMapObject.fromJson; + final cubit = ListCubitList(fromJson, true); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitList(fromJson).state, + [item], + ); + }, storage: storage); }); test('persists and restores obj->list list correctly', () async { - final item = ListListObject(1); - const fromJson = ListListObject.fromJson; - final cubit = ListCubitList(fromJson); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitList(fromJson).state, - [item], - ); + await HydratedBlocOverrides.runZoned(() async { + final item = ListListObject(1); + const fromJson = ListListObject.fromJson; + final cubit = ListCubitList(fromJson); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitList(fromJson).state, + [item], + ); + }, storage: storage); }); test('persists and restores obj-*>list list correctly', () async { - final item = ListListObject(1); - const fromJson = ListListObject.fromJson; - final cubit = ListCubitList(fromJson, true); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitList(fromJson).state, - [item], - ); + await HydratedBlocOverrides.runZoned(() async { + final item = ListListObject(1); + const fromJson = ListListObject.fromJson; + final cubit = ListCubitList( + fromJson, + true, + ); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitList(fromJson).state, + [item], + ); + }, storage: storage); }); test('persists and restores obj->list list correctly', () async { - final item = ListCustomObject(1); - const fromJson = ListCustomObject.fromJson; - final cubit = ListCubitList(fromJson); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitList(fromJson).state, - [item], - ); + await HydratedBlocOverrides.runZoned(() async { + final item = ListCustomObject(1); + const fromJson = ListCustomObject.fromJson; + final cubit = ListCubitList(fromJson); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitList(fromJson).state, + [item], + ); + }, storage: storage); }); test('persists and restores obj-*>list list correctly', () async { - final item = ListCustomObject(1); - const fromJson = ListCustomObject.fromJson; - final cubit = - ListCubitList(fromJson, true); - expect(cubit.state, isEmpty); - cubit.addItem(item); - await sleep(); - expect( - ListCubitList(fromJson).state, - [item], - ); + await HydratedBlocOverrides.runZoned(() async { + final item = ListCustomObject(1); + const fromJson = ListCustomObject.fromJson; + final cubit = + ListCubitList(fromJson, true); + expect(cubit.state, isEmpty); + cubit.addItem(item); + await sleep(); + expect( + ListCubitList(fromJson).state, + [item], + ); + }, storage: storage); }); test('persists and restores obj->list empty list correctly', () async { - const fromJson = ListCustomObject.fromJson; - final cubit = ListCubitList(fromJson); - expect(cubit.state, isEmpty); - cubit.reset(); - await sleep(); - expect( - ListCubitList(fromJson).state, - isEmpty, - ); + await HydratedBlocOverrides.runZoned(() async { + const fromJson = ListCustomObject.fromJson; + final cubit = ListCubitList(fromJson); + expect(cubit.state, isEmpty); + cubit.reset(); + await sleep(); + expect( + ListCubitList(fromJson).state, + isEmpty, + ); + }, storage: storage); }); test('persists and restores obj-*>list empty list correctly', () async { - const fromJson = ListCustomObject.fromJson; - final cubit = - ListCubitList(fromJson, true); - expect(cubit.state, isEmpty); - cubit.reset(); - await sleep(); - expect( - ListCubitList(fromJson).state, - isEmpty, - ); + await HydratedBlocOverrides.runZoned(() async { + const fromJson = ListCustomObject.fromJson; + final cubit = + ListCubitList(fromJson, true); + expect(cubit.state, isEmpty); + cubit.reset(); + await sleep(); + expect( + ListCubitList(fromJson).state, + isEmpty, + ); + }, storage: storage); }); }); group('ManualCubit', () { test('persists and restores state correctly', () async { - const dog = Dog('Rover', 5, [Toy('Ball')]); - final cubit = ManualCubit(); - expect(cubit.state, isNull); - cubit.setDog(dog); - await sleep(); - expect(ManualCubit().state, dog); + await HydratedBlocOverrides.runZoned(() async { + const dog = Dog('Rover', 5, [Toy('Ball')]); + final cubit = ManualCubit(); + expect(cubit.state, isNull); + cubit.setDog(dog); + await sleep(); + expect(ManualCubit().state, dog); + }, storage: storage); }); }); group('SimpleCubit', () { test('persists and restores state correctly', () async { - final cubit = SimpleCubit(); - expect(cubit.state, 0); - cubit.increment(); - expect(cubit.state, 1); - await sleep(); - expect(SimpleCubit().state, 1); + await HydratedBlocOverrides.runZoned(() async { + final cubit = SimpleCubit(); + expect(cubit.state, 0); + cubit.increment(); + expect(cubit.state, 1); + await sleep(); + expect(SimpleCubit().state, 1); + }, storage: storage); }); test('does not throw after clear', () async { - final cubit = SimpleCubit(); - expect(cubit.state, 0); - cubit.increment(); - expect(cubit.state, 1); - await storage.clear(); - expect(SimpleCubit().state, 0); + await HydratedBlocOverrides.runZoned(() async { + final cubit = SimpleCubit(); + expect(cubit.state, 0); + cubit.increment(); + expect(cubit.state, 1); + await storage.clear(); + expect(SimpleCubit().state, 0); + }, storage: storage); }); }); group('CyclicCubit', () { test('throws cyclic error', () async { - final cycle2 = Cycle2(); - final cycle1 = Cycle1(cycle2); - cycle2.cycle1 = cycle1; - final cubit = CyclicCubit(); - expect(cubit.state, isNull); - expect( - () => cubit.setCyclic(cycle1), - throwsA(isA().having( - (dynamic e) => e.error, - 'inner error of cubit error', - isA().having( - (dynamic e) => e.cause, - 'cycle2 -> cycle1 -> cycle2 ->', - isA(), + await HydratedBlocOverrides.runZoned(() async { + final cycle2 = Cycle2(); + final cycle1 = Cycle1(cycle2); + cycle2.cycle1 = cycle1; + final cubit = CyclicCubit(); + expect(cubit.state, isNull); + expect( + () => cubit.setCyclic(cycle1), + throwsA( + isA().having( + (dynamic e) => e.cause, + 'cycle2 -> cycle1 -> cycle2 ->', + isA(), + ), ), - )), - ); + ); + }, storage: storage); }); }); group('BadCubit', () { test('throws unsupported object: no `toJson`', () async { - final cubit = BadCubit(); - expect(cubit.state, isNull); - expect( - cubit.setBad, - throwsA(isA().having( - (dynamic e) => e.error, - 'inner error of cubit error', - isA().having( - (dynamic e) => e.cause, - 'Object has no `toJson`', - isA(), + await HydratedBlocOverrides.runZoned(() async { + final cubit = BadCubit(); + expect(cubit.state, isNull); + expect( + cubit.setBad, + throwsA( + isA().having( + (dynamic e) => e.cause, + 'Object has no `toJson`', + isA(), + ), ), - )), - ); + ); + }, storage: storage); }); test('throws unsupported object: bad `toJson`', () async { - final cubit = BadCubit(); - expect(cubit.state, isNull); - expect( - () => cubit.setBad(VeryBadObject()), - throwsA(isA().having( - (dynamic e) => e.error, - 'inner error of cubit error', - isA(), - )), - ); + await HydratedBlocOverrides.runZoned(() async { + final cubit = BadCubit(); + expect(cubit.state, isNull); + expect( + () => cubit.setBad(VeryBadObject()), + throwsA(isA()), + ); + }, storage: storage); }); }); }); diff --git a/packages/hydrated_bloc/test/hydrated_bloc_overrides_test.dart b/packages/hydrated_bloc/test/hydrated_bloc_overrides_test.dart new file mode 100644 index 00000000000..e2d31494319 --- /dev/null +++ b/packages/hydrated_bloc/test/hydrated_bloc_overrides_test.dart @@ -0,0 +1,195 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc/src/bloc.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class FakeBlocObserver extends Fake implements BlocObserver {} + +class FakeStorage extends Fake implements Storage {} + +void main() { + group('HydratedBlocOverrides', () { + group('runZoned', () { + test('uses default BlocObserver when not specified', () { + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.blocObserver, isA()); + }); + }); + + test('uses default EventTransformer when not specified', () { + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.eventTransformer, isA()); + }); + }); + + test('uses default storage when not specified', () { + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.storage, isA()); + }); + }); + + test('uses custom BlocObserver when specified', () { + final blocObserver = FakeBlocObserver(); + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.blocObserver, equals(blocObserver)); + }, blocObserver: blocObserver); + }); + + test('uses custom EventTransformer when specified', () { + final eventTransformer = (Stream events, EventMapper mapper) { + return events.asyncExpand(mapper); + }; + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.eventTransformer, equals(eventTransformer)); + }, eventTransformer: eventTransformer); + }); + + test('uses custom storage when specified', () { + final storage = FakeStorage(); + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.storage, equals(storage)); + }, storage: storage); + }); + + test( + 'uses current BlocObserver when not specified ' + 'and zone already contains a BlocObserver', () { + final blocObserver = FakeBlocObserver(); + HydratedBlocOverrides.runZoned(() { + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.blocObserver, equals(blocObserver)); + }); + }, blocObserver: blocObserver); + }); + + test( + 'uses current EventTransformer when not specified ' + 'and zone already contains an EventTransformer', () { + final eventTransformer = (Stream events, EventMapper mapper) { + return events.asyncExpand(mapper); + }; + HydratedBlocOverrides.runZoned(() { + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.eventTransformer, equals(eventTransformer)); + }); + }, eventTransformer: eventTransformer); + }); + + test( + 'uses nested BlocObserver when specified ' + 'and zone already contains a BlocObserver', () { + final rootBlocObserver = FakeBlocObserver(); + HydratedBlocOverrides.runZoned(() { + final nestedBlocObserver = FakeBlocObserver(); + final overrides = HydratedBlocOverrides.current; + expect(overrides!.blocObserver, equals(rootBlocObserver)); + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.blocObserver, equals(nestedBlocObserver)); + }, blocObserver: nestedBlocObserver); + }, blocObserver: rootBlocObserver); + }); + + test( + 'uses nested EventTransformer when specified ' + 'and zone already contains an EventTransformer', () { + final rootEventTransformer = (Stream events, EventMapper mapper) { + return events.asyncExpand(mapper); + }; + HydratedBlocOverrides.runZoned(() { + final nestedEventTransformer = (Stream events, EventMapper mapper) { + return events.asyncExpand(mapper); + }; + final overrides = HydratedBlocOverrides.current; + expect(overrides!.eventTransformer, equals(rootEventTransformer)); + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.eventTransformer, equals(nestedEventTransformer)); + }, eventTransformer: nestedEventTransformer); + }, eventTransformer: rootEventTransformer); + }); + + test( + 'uses nested storage when specified ' + 'and zone already contains a storage', () { + final rootStorage = FakeStorage(); + HydratedBlocOverrides.runZoned(() { + final nestedStorage = FakeStorage(); + final overrides = HydratedBlocOverrides.current; + expect(overrides!.storage, equals(rootStorage)); + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.storage, equals(nestedStorage)); + }, storage: nestedStorage); + }, storage: rootStorage); + }); + + test('uses parent storage when nested zone does not specify', () { + final storage = FakeStorage(); + HydratedBlocOverrides.runZoned(() { + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current; + expect(overrides!.storage, equals(storage)); + }); + }, storage: storage); + }); + + test( + 'uses parent BlocOverrides BlocObserver ' + 'when nested zone does not specify', () { + final blocObserver = FakeBlocObserver(); + BlocOverrides.runZoned(() { + HydratedBlocOverrides.runZoned(() { + final hydratedOverrides = HydratedBlocOverrides.current; + expect(hydratedOverrides!.blocObserver, equals(blocObserver)); + final overrides = BlocOverrides.current; + expect(overrides!.blocObserver, equals(blocObserver)); + }); + }, blocObserver: blocObserver); + }); + + test( + 'uses nested BlocObserver ' + 'when nested zone does specify and parent BlocOverrides ' + 'specifies a different BlocObserver', () { + final storage = FakeStorage(); + final rootBlocObserver = FakeBlocObserver(); + final nestedBlocObserver = FakeBlocObserver(); + BlocOverrides.runZoned(() { + final overrides = BlocOverrides.current; + expect(overrides!.blocObserver, equals(rootBlocObserver)); + HydratedBlocOverrides.runZoned( + () { + final overrides = HydratedBlocOverrides.current!; + expect(overrides.blocObserver, equals(nestedBlocObserver)); + expect(overrides.storage, equals(storage)); + }, + blocObserver: nestedBlocObserver, + storage: storage, + ); + }, blocObserver: rootBlocObserver); + }); + + test('overrides cannot be mutated after zone is created', () { + final originalStorage = FakeStorage(); + final otherStorage = FakeStorage(); + var storage = originalStorage; + HydratedBlocOverrides.runZoned(() { + storage = otherStorage; + final overrides = HydratedBlocOverrides.current!; + expect(overrides.storage, equals(originalStorage)); + expect(overrides.storage, isNot(equals(otherStorage))); + }, storage: storage); + }); + }); + }); +} diff --git a/packages/hydrated_bloc/test/hydrated_bloc_test.dart b/packages/hydrated_bloc/test/hydrated_bloc_test.dart index 9a92193496b..af1a8e82ebe 100644 --- a/packages/hydrated_bloc/test/hydrated_bloc_test.dart +++ b/packages/hydrated_bloc/test/hydrated_bloc_test.dart @@ -1,12 +1,12 @@ -// ignore_for_file: invalid_use_of_protected_member import 'dart:async'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:pedantic/pedantic.dart'; import 'package:test/test.dart'; import 'package:uuid/uuid.dart'; +void unawaited(Future? _) {} + class MockStorage extends Mock implements Storage {} class MyUuidHydratedBloc extends HydratedBloc { @@ -126,332 +126,378 @@ void main() { when(() => storage.write(any(), any())).thenAnswer((_) async {}); when(() => storage.delete(any())).thenAnswer((_) async {}); when(() => storage.clear()).thenAnswer((_) async {}); - HydratedBloc.storage = storage; }); test('storage getter returns correct storage instance', () { final storage = MockStorage(); - HydratedBloc.storage = storage; - expect(HydratedBloc.storage, storage); + HydratedBlocOverrides.runZoned(() { + expect(HydratedBlocOverrides.current!.storage, equals(storage)); + }, storage: storage); }); test('reads from storage once upon initialization', () { - MyCallbackHydratedBloc(); - verify(() => storage.read('MyCallbackHydratedBloc')).called(1); + HydratedBlocOverrides.runZoned(() { + MyCallbackHydratedBloc(); + verify(() => storage.read('MyCallbackHydratedBloc')).called(1); + }, storage: storage); }); test( 'does not read from storage on subsequent state changes ' 'when cache value exists', () async { - when(() => storage.read(any())).thenReturn({'value': 42}); - final bloc = MyCallbackHydratedBloc(); - expect(bloc.state, 42); - bloc.add(Increment()); - await expectLater(bloc.stream, emitsInOrder(const [43])); - verify(() => storage.read('MyCallbackHydratedBloc')).called(1); + await HydratedBlocOverrides.runZoned(() async { + when(() => storage.read(any())).thenReturn({'value': 42}); + final bloc = MyCallbackHydratedBloc(); + expect(bloc.state, 42); + bloc.add(Increment()); + await expectLater(bloc.stream, emitsInOrder(const [43])); + verify(() => storage.read('MyCallbackHydratedBloc')).called(1); + }, storage: storage); }); test( 'does not deserialize state on subsequent state changes ' 'when cache value exists', () async { - final fromJsonCalls = []; - when(() => storage.read(any())).thenReturn({'value': 42}); - final bloc = MyCallbackHydratedBloc( - onFromJsonCalled: fromJsonCalls.add, - ); - expect(bloc.state, 42); - bloc.add(Increment()); - await expectLater(bloc.stream, emitsInOrder(const [43])); - expect(fromJsonCalls, [ - {'value': 42} - ]); + await HydratedBlocOverrides.runZoned(() async { + final fromJsonCalls = []; + when(() => storage.read(any())).thenReturn({'value': 42}); + final bloc = MyCallbackHydratedBloc( + onFromJsonCalled: fromJsonCalls.add, + ); + expect(bloc.state, 42); + bloc.add(Increment()); + await expectLater(bloc.stream, emitsInOrder(const [43])); + expect(fromJsonCalls, [ + {'value': 42} + ]); + }, storage: storage); }); test( 'does not read from storage on subsequent state changes ' 'when cache is empty', () async { - when(() => storage.read(any())).thenReturn(null); - final bloc = MyCallbackHydratedBloc(); - expect(bloc.state, 0); - bloc.add(Increment()); - await expectLater(bloc.stream, emitsInOrder(const [1])); - verify(() => storage.read('MyCallbackHydratedBloc')).called(1); + await HydratedBlocOverrides.runZoned(() async { + when(() => storage.read(any())).thenReturn(null); + final bloc = MyCallbackHydratedBloc(); + expect(bloc.state, 0); + bloc.add(Increment()); + await expectLater(bloc.stream, emitsInOrder(const [1])); + verify(() => storage.read('MyCallbackHydratedBloc')).called(1); + }, storage: storage); }); test('does not deserialize state when cache is empty', () async { - final fromJsonCalls = []; - when(() => storage.read(any())).thenReturn(null); - final bloc = MyCallbackHydratedBloc( - onFromJsonCalled: fromJsonCalls.add, - ); - expect(bloc.state, 0); - bloc.add(Increment()); - await expectLater(bloc.stream, emitsInOrder(const [1])); - expect(fromJsonCalls, isEmpty); + await HydratedBlocOverrides.runZoned(() async { + final fromJsonCalls = []; + when(() => storage.read(any())).thenReturn(null); + final bloc = MyCallbackHydratedBloc( + onFromJsonCalled: fromJsonCalls.add, + ); + expect(bloc.state, 0); + bloc.add(Increment()); + await expectLater(bloc.stream, emitsInOrder(const [1])); + expect(fromJsonCalls, isEmpty); + }, storage: storage); }); test( 'does not read from storage on subsequent state changes ' 'when cache is malformed', () async { - unawaited(runZonedGuarded(() async { - when(() => storage.read(any())).thenReturn('{'); - MyCallbackHydratedBloc().add(Increment()); - }, (_, __) { - verify(() => storage.read('MyCallbackHydratedBloc')).called(1); - })); + await HydratedBlocOverrides.runZoned(() async { + unawaited(runZonedGuarded(() async { + when(() => storage.read(any())).thenReturn('{'); + MyCallbackHydratedBloc().add(Increment()); + }, (_, __) { + verify(() => storage.read('MyCallbackHydratedBloc')) + .called(1); + })); + }, storage: storage); }); test('does not deserialize state when cache is malformed', () async { - final fromJsonCalls = []; - unawaited(runZonedGuarded(() async { - when(() => storage.read(any())).thenReturn('{'); - MyCallbackHydratedBloc( - onFromJsonCalled: fromJsonCalls.add, - ).add(Increment()); - expect(fromJsonCalls, isEmpty); - }, (_, __) { - expect(fromJsonCalls, isEmpty); - })); + await HydratedBlocOverrides.runZoned(() async { + final fromJsonCalls = []; + unawaited(runZonedGuarded(() async { + when(() => storage.read(any())).thenReturn('{'); + MyCallbackHydratedBloc( + onFromJsonCalled: fromJsonCalls.add, + ).add(Increment()); + expect(fromJsonCalls, isEmpty); + }, (_, __) { + expect(fromJsonCalls, isEmpty); + })); + }, storage: storage); }); group('SingleHydratedBloc', () { test('should call storage.write when onChange is called', () { - const change = Change( - currentState: 0, - nextState: 0, - ); - const expected = {'value': 0}; - MyHydratedBloc().onChange(change); - verify(() => storage.write('MyHydratedBloc', expected)).called(2); + HydratedBlocOverrides.runZoned(() { + const change = Change( + currentState: 0, + nextState: 0, + ); + const expected = {'value': 0}; + MyHydratedBloc().onChange(change); + verify(() => storage.write('MyHydratedBloc', expected)).called(2); + }, storage: storage); }); test('should call storage.write when onChange is called with bloc id', () { - final bloc = MyHydratedBloc('A'); - const change = Change( - currentState: 0, - nextState: 0, - ); - const expected = {'value': 0}; - bloc.onChange(change); - verify(() => storage.write('MyHydratedBlocA', expected)).called(2); - }); - - test('should call onError when storage.write throws', () { - runZonedGuarded(() async { - final expectedError = Exception('oops'); + HydratedBlocOverrides.runZoned(() { + final bloc = MyHydratedBloc('A'); const change = Change( currentState: 0, nextState: 0, ); - final bloc = MyHydratedBloc(); - when( - () => storage.write(any(), any()), - ).thenThrow(expectedError); + const expected = {'value': 0}; bloc.onChange(change); - await Future.delayed(const Duration(milliseconds: 300)); - verify(() => bloc.onError(expectedError, any())).called(2); - }, (error, _) { - expect( - (error as BlocUnhandledErrorException).error.toString(), - 'Exception: oops', - ); - expect((error).stackTrace, isNotNull); - }); + verify(() => storage.write('MyHydratedBlocA', expected)).called(2); + }, storage: storage); + }); + + test('should call onError when storage.write throws', () { + HydratedBlocOverrides.runZoned(() { + runZonedGuarded(() async { + final expectedError = Exception('oops'); + const change = Change( + currentState: 0, + nextState: 0, + ); + final bloc = MyHydratedBloc(); + when( + () => storage.write(any(), any()), + ).thenThrow(expectedError); + bloc.onChange(change); + await Future.delayed(const Duration(milliseconds: 300)); + // ignore: invalid_use_of_protected_member + verify(() => bloc.onError(expectedError, any())).called(2); + }, (error, _) { + expect(error.toString(), 'Exception: oops'); + }); + }, storage: storage); }); test('stores initial state when instantiated', () { - MyHydratedBloc(); - verify( - () => storage.write('MyHydratedBloc', {'value': 0}), - ).called(1); + HydratedBlocOverrides.runZoned(() { + MyHydratedBloc(); + verify( + () => storage.write('MyHydratedBloc', {'value': 0}), + ).called(1); + }, storage: storage); }); test('initial state should return 0 when fromJson returns null', () { - when(() => storage.read(any())).thenReturn(null); - expect(MyHydratedBloc().state, 0); - verify(() => storage.read('MyHydratedBloc')).called(1); + HydratedBlocOverrides.runZoned(() { + when(() => storage.read(any())).thenReturn(null); + expect(MyHydratedBloc().state, 0); + verify(() => storage.read('MyHydratedBloc')).called(1); + }, storage: storage); }); test('initial state should return 101 when fromJson returns 101', () { - when(() => storage.read(any())).thenReturn({'value': 101}); - expect(MyHydratedBloc().state, 101); - verify(() => storage.read('MyHydratedBloc')).called(1); + HydratedBlocOverrides.runZoned(() { + when(() => storage.read(any())).thenReturn({'value': 101}); + expect(MyHydratedBloc().state, 101); + verify(() => storage.read('MyHydratedBloc')).called(1); + }, storage: storage); }); group('clear', () { test('calls delete on storage', () async { - await MyHydratedBloc().clear(); - verify(() => storage.delete('MyHydratedBloc')).called(1); + await HydratedBlocOverrides.runZoned(() async { + await MyHydratedBloc().clear(); + verify(() => storage.delete('MyHydratedBloc')).called(1); + }, storage: storage); }); }); }); group('MultiHydratedBloc', () { test('initial state should return 0 when fromJson returns null', () { - when(() => storage.read(any())).thenReturn(null); - expect(MyMultiHydratedBloc('A').state, 0); - verify(() => storage.read('MyMultiHydratedBlocA')).called(1); - - expect(MyMultiHydratedBloc('B').state, 0); - verify(() => storage.read('MyMultiHydratedBlocB')).called(1); + HydratedBlocOverrides.runZoned(() { + when(() => storage.read(any())).thenReturn(null); + expect(MyMultiHydratedBloc('A').state, 0); + verify(() => storage.read('MyMultiHydratedBlocA')).called(1); + + expect(MyMultiHydratedBloc('B').state, 0); + verify(() => storage.read('MyMultiHydratedBlocB')).called(1); + }, storage: storage); }); test('initial state should return 101/102 when fromJson returns 101/102', () { - when( - () => storage.read('MyMultiHydratedBlocA'), - ).thenReturn({'value': 101}); - expect(MyMultiHydratedBloc('A').state, 101); - verify(() => storage.read('MyMultiHydratedBlocA')).called(1); - - when( - () => storage.read('MyMultiHydratedBlocB'), - ).thenReturn({'value': 102}); - expect(MyMultiHydratedBloc('B').state, 102); - verify(() => storage.read('MyMultiHydratedBlocB')).called(1); + HydratedBlocOverrides.runZoned(() { + when( + () => storage.read('MyMultiHydratedBlocA'), + ).thenReturn({'value': 101}); + expect(MyMultiHydratedBloc('A').state, 101); + verify(() => storage.read('MyMultiHydratedBlocA')).called(1); + + when( + () => storage.read('MyMultiHydratedBlocB'), + ).thenReturn({'value': 102}); + expect(MyMultiHydratedBloc('B').state, 102); + verify(() => storage.read('MyMultiHydratedBlocB')).called(1); + }, storage: storage); }); group('clear', () { test('calls delete on storage', () async { - await MyMultiHydratedBloc('A').clear(); - verify(() => storage.delete('MyMultiHydratedBlocA')).called(1); - verifyNever(() => storage.delete('MyMultiHydratedBlocB')); - - await MyMultiHydratedBloc('B').clear(); - verify(() => storage.delete('MyMultiHydratedBlocB')).called(1); + await HydratedBlocOverrides.runZoned(() async { + await MyMultiHydratedBloc('A').clear(); + verify(() => storage.delete('MyMultiHydratedBlocA')).called(1); + verifyNever(() => storage.delete('MyMultiHydratedBlocB')); + + await MyMultiHydratedBloc('B').clear(); + verify(() => storage.delete('MyMultiHydratedBlocB')).called(1); + }, storage: storage); }); }); }); group('MyUuidHydratedBloc', () { test('stores initial state when instantiated', () async { - when( - () => storage.write(any(), any>()), - ).thenAnswer((_) async {}); - MyUuidHydratedBloc(); - await untilCalled( - () => storage.write(any(), any>()), - ); - verify( - () => storage.write( - 'MyUuidHydratedBloc', - any>(), - ), - ).called(1); + await HydratedBlocOverrides.runZoned(() async { + when( + () => storage.write(any(), any>()), + ).thenAnswer((_) async {}); + MyUuidHydratedBloc(); + await untilCalled( + () => storage.write(any(), any>()), + ); + verify( + () => storage.write( + 'MyUuidHydratedBloc', + any>(), + ), + ).called(1); + }, storage: storage); }); test('correctly caches computed initial state', () async { - dynamic cachedState; - when(() => storage.read(any())).thenReturn(cachedState); - when( - () => storage.write(any(), any()), - ).thenAnswer((_) => Future.value()); - MyUuidHydratedBloc(); - final captured = verify( - () => storage.write('MyUuidHydratedBloc', captureAny()), - ).captured; - cachedState = captured.first; - when(() => storage.read(any())).thenReturn(cachedState); - MyUuidHydratedBloc(); - final secondCaptured = verify( - () => storage.write('MyUuidHydratedBloc', captureAny()), - ).captured; - final dynamic initialStateB = secondCaptured.first; - - expect(initialStateB, cachedState); + await HydratedBlocOverrides.runZoned(() async { + dynamic cachedState; + when(() => storage.read(any())).thenReturn(cachedState); + when( + () => storage.write(any(), any()), + ).thenAnswer((_) => Future.value()); + MyUuidHydratedBloc(); + final captured = verify( + () => storage.write('MyUuidHydratedBloc', captureAny()), + ).captured; + cachedState = captured.first; + when(() => storage.read(any())).thenReturn(cachedState); + MyUuidHydratedBloc(); + final secondCaptured = verify( + () => storage.write('MyUuidHydratedBloc', captureAny()), + ).captured; + final dynamic initialStateB = secondCaptured.first; + + expect(initialStateB, cachedState); + }, storage: storage); }); }); group('MyErrorThrowingBloc', () { test('continues to emit new states when serialization fails', () async { - unawaited(runZonedGuarded( - () async { - final bloc = MyErrorThrowingBloc(); - final expectedStates = [0, 1, emitsDone]; - unawaited(expectLater(bloc.stream, emitsInOrder(expectedStates))); - bloc.add(Object); - await bloc.close(); - }, - (_, __) {}, - )); + await HydratedBlocOverrides.runZoned(() async { + await runZonedGuarded( + () async { + final bloc = MyErrorThrowingBloc(); + final expectedStates = [0, 1, emitsDone]; + unawaited(expectLater(bloc.stream, emitsInOrder(expectedStates))); + bloc.add(Object); + await bloc.close(); + }, + (_, __) {}, + ); + }, storage: storage); }); test('calls onError when json decode fails', () async { - Object? lastError; - StackTrace? lastStackTrace; - unawaited(runZonedGuarded(() async { - when(() => storage.read(any())).thenReturn('invalid json'); - MyErrorThrowingBloc( - onErrorCallback: (error, stackTrace) { - lastError = error; - lastStackTrace = stackTrace; - }, - ); - }, (_, __) { - expect(lastStackTrace, isNotNull); - expect( - lastError.toString().startsWith( - '''Unhandled error type \'String\' is not a subtype of type \'Map?\' in type cast''', - ), - isTrue, - ); - })); + await HydratedBlocOverrides.runZoned(() async { + Object? lastError; + StackTrace? lastStackTrace; + await runZonedGuarded(() async { + when(() => storage.read(any())).thenReturn('invalid json'); + MyErrorThrowingBloc( + onErrorCallback: (error, stackTrace) { + lastError = error; + lastStackTrace = stackTrace; + }, + ); + }, (_, __) { + expect(lastStackTrace, isNotNull); + expect( + lastError.toString().startsWith( + '''Unhandled error type \'String\' is not a subtype of type \'Map?\' in type cast''', + ), + isTrue, + ); + }); + }, storage: storage); }); test('returns super.state when json decode fails', () async { - MyErrorThrowingBloc? bloc; - unawaited(runZonedGuarded(() async { - when(() => storage.read(any())).thenReturn('invalid json'); - bloc = MyErrorThrowingBloc(superOnError: false); - }, (_, __) { - expect(bloc?.state, 0); - })); + await HydratedBlocOverrides.runZoned(() async { + MyErrorThrowingBloc? bloc; + await runZonedGuarded(() async { + when(() => storage.read(any())).thenReturn('invalid json'); + bloc = MyErrorThrowingBloc(superOnError: false); + }, (_, __) { + expect(bloc?.state, 0); + }); + }, storage: storage); }); test('calls onError when storage.write fails', () async { - Object? lastError; - StackTrace? lastStackTrace; - final exception = Exception('oops'); - unawaited(runZonedGuarded(() async { - when(() => storage.write(any(), any())).thenThrow(exception); - MyErrorThrowingBloc( - onErrorCallback: (error, stackTrace) { - lastError = error; - lastStackTrace = stackTrace; - }, - ); - }, (error, _) { - expect(lastError, isA()); - expect(lastStackTrace, isNotNull); - expect( - (error as BlocUnhandledErrorException).error.toString(), - '''Converting object to an encodable object failed: Object''', - ); - expect(error.stackTrace, isNotNull); - })); - }); - - test('calls onError when json encode fails', () async { - unawaited(runZonedGuarded( - () async { - Object? lastError; - StackTrace? lastStackTrace; - final bloc = MyErrorThrowingBloc( + await HydratedBlocOverrides.runZoned(() async { + Object? lastError; + StackTrace? lastStackTrace; + final exception = Exception('oops'); + await runZonedGuarded(() async { + when(() => storage.write(any(), any())) + .thenThrow(exception); + MyErrorThrowingBloc( onErrorCallback: (error, stackTrace) { lastError = error; lastStackTrace = stackTrace; }, - )..add(Object); - await bloc.close(); - expect( - '$lastError', - 'Converting object to an encodable object failed: Object', ); + }, (error, _) { + expect(lastError, isA()); expect(lastStackTrace, isNotNull); - }, - (_, __) {}, - )); + expect( + error.toString(), + '''Converting object to an encodable object failed: Object''', + ); + }); + }, storage: storage); + }); + + test('calls onError when json encode fails', () async { + await HydratedBlocOverrides.runZoned(() async { + await runZonedGuarded( + () async { + Object? lastError; + StackTrace? lastStackTrace; + final bloc = MyErrorThrowingBloc( + onErrorCallback: (error, stackTrace) { + lastError = error; + lastStackTrace = stackTrace; + }, + )..add(Object); + await bloc.close(); + expect( + '$lastError', + 'Converting object to an encodable object failed: Object', + ); + expect(lastStackTrace, isNotNull); + }, + (_, __) {}, + ); + }, storage: storage); }); }); }); diff --git a/packages/hydrated_bloc/test/hydrated_cubit_test.dart b/packages/hydrated_bloc/test/hydrated_cubit_test.dart index 9a720c6acd3..570f71a2090 100644 --- a/packages/hydrated_bloc/test/hydrated_cubit_test.dart +++ b/packages/hydrated_bloc/test/hydrated_cubit_test.dart @@ -86,97 +86,123 @@ void main() { when(() => storage.read(any())).thenReturn({}); when(() => storage.delete(any())).thenAnswer((_) async {}); when(() => storage.clear()).thenAnswer((_) async {}); - HydratedBloc.storage = storage; }); test('reads from storage once upon initialization', () { - MyCallbackHydratedCubit(); - verify(() => storage.read('MyCallbackHydratedCubit')).called(1); + HydratedBlocOverrides.runZoned(() { + MyCallbackHydratedCubit(); + verify( + () => storage.read('MyCallbackHydratedCubit'), + ).called(1); + }, storage: storage); }); test( 'does not read from storage on subsequent state changes ' 'when cache value exists', () { - when(() => storage.read(any())).thenReturn({'value': 42}); - final cubit = MyCallbackHydratedCubit(); - expect(cubit.state, 42); - cubit.increment(); - expect(cubit.state, 43); - verify(() => storage.read('MyCallbackHydratedCubit')).called(1); + HydratedBlocOverrides.runZoned(() { + when(() => storage.read(any())).thenReturn({'value': 42}); + final cubit = MyCallbackHydratedCubit(); + expect(cubit.state, 42); + cubit.increment(); + expect(cubit.state, 43); + verify(() => storage.read('MyCallbackHydratedCubit')) + .called(1); + }, storage: storage); }); test( 'does not deserialize state on subsequent state changes ' 'when cache value exists', () { - final fromJsonCalls = []; - when(() => storage.read(any())).thenReturn({'value': 42}); - final cubit = MyCallbackHydratedCubit( - onFromJsonCalled: fromJsonCalls.add, - ); - expect(cubit.state, 42); - cubit.increment(); - expect(cubit.state, 43); - expect(fromJsonCalls, [ - {'value': 42} - ]); + HydratedBlocOverrides.runZoned(() { + final fromJsonCalls = []; + when(() => storage.read(any())).thenReturn({'value': 42}); + final cubit = MyCallbackHydratedCubit( + onFromJsonCalled: fromJsonCalls.add, + ); + expect(cubit.state, 42); + cubit.increment(); + expect(cubit.state, 43); + expect(fromJsonCalls, [ + {'value': 42} + ]); + }, storage: storage); }); test( 'does not read from storage on subsequent state changes ' 'when cache is empty', () { - when(() => storage.read(any())).thenReturn(null); - final cubit = MyCallbackHydratedCubit(); - expect(cubit.state, 0); - cubit.increment(); - expect(cubit.state, 1); - verify(() => storage.read('MyCallbackHydratedCubit')).called(1); + HydratedBlocOverrides.runZoned(() { + when(() => storage.read(any())).thenReturn(null); + final cubit = MyCallbackHydratedCubit(); + expect(cubit.state, 0); + cubit.increment(); + expect(cubit.state, 1); + verify(() => storage.read('MyCallbackHydratedCubit')) + .called(1); + }, storage: storage); }); test('does not deserialize state when cache is empty', () { - final fromJsonCalls = []; - when(() => storage.read(any())).thenReturn(null); - final cubit = MyCallbackHydratedCubit( - onFromJsonCalled: fromJsonCalls.add, - ); - expect(cubit.state, 0); - cubit.increment(); - expect(cubit.state, 1); - expect(fromJsonCalls, isEmpty); + HydratedBlocOverrides.runZoned(() { + final fromJsonCalls = []; + when(() => storage.read(any())).thenReturn(null); + final cubit = MyCallbackHydratedCubit( + onFromJsonCalled: fromJsonCalls.add, + ); + expect(cubit.state, 0); + cubit.increment(); + expect(cubit.state, 1); + expect(fromJsonCalls, isEmpty); + }, storage: storage); }); test( 'does not read from storage on subsequent state changes ' 'when cache is malformed', () { - when(() => storage.read(any())).thenReturn('{'); - final cubit = MyCallbackHydratedCubit(); - expect(cubit.state, 0); - cubit.increment(); - expect(cubit.state, 1); - verify(() => storage.read('MyCallbackHydratedCubit')).called(1); + HydratedBlocOverrides.runZoned(() { + when(() => storage.read(any())).thenReturn('{'); + final cubit = MyCallbackHydratedCubit(); + expect(cubit.state, 0); + cubit.increment(); + expect(cubit.state, 1); + verify(() => storage.read('MyCallbackHydratedCubit')) + .called(1); + }, storage: storage); }); test('does not deserialize state when cache is malformed', () { - final fromJsonCalls = []; - runZonedGuarded( - () { - when(() => storage.read(any())).thenReturn('{'); - MyCallbackHydratedCubit(onFromJsonCalled: fromJsonCalls.add); - }, - (_, __) { - expect(fromJsonCalls, isEmpty); - }, - ); + HydratedBlocOverrides.runZoned(() { + final fromJsonCalls = []; + runZonedGuarded( + () { + when(() => storage.read(any())).thenReturn('{'); + MyCallbackHydratedCubit(onFromJsonCalled: fromJsonCalls.add); + }, + (_, __) { + expect(fromJsonCalls, isEmpty); + }, + ); + }, storage: storage); }); group('SingleHydratedCubit', () { test('should throw StorageNotFound when storage is null', () { - HydratedBloc.storage = null; expect( () => MyHydratedCubit(), throwsA(isA()), ); }); + test('should throw StorageNotFound when storage is default', () { + HydratedBlocOverrides.runZoned(() { + expect( + () => MyHydratedCubit(), + throwsA(isA()), + ); + }); + }); + test('StorageNotFound overrides toString', () { expect( // ignore: prefer_const_constructors @@ -184,158 +210,195 @@ void main() { 'Storage was accessed before it was initialized.\n' 'Please ensure that storage has been initialized.\n\n' 'For example:\n\n' - 'HydratedBloc.storage = await HydratedStorage.build();', + 'final storage = await HydratedStorage.build();\n' + 'HydratedBlocOverrides.runZoned(\n' + ' () => runApp(MyApp()),\n' + ' storage: storage,\n' + ');', ); }); test('storage getter returns correct storage instance', () { final storage = MockStorage(); - HydratedBloc.storage = storage; - expect(HydratedBloc.storage, storage); + HydratedBlocOverrides.runZoned(() { + expect(HydratedBlocOverrides.current!.storage, equals(storage)); + }, storage: storage); }); test('should call storage.write when onChange is called', () { - final transition = const Change( - currentState: 0, - nextState: 0, - ); - final expected = {'value': 0}; - MyHydratedCubit().onChange(transition); - verify(() => storage.write('MyHydratedCubit', expected)).called(2); + HydratedBlocOverrides.runZoned(() { + final transition = const Change( + currentState: 0, + nextState: 0, + ); + final expected = {'value': 0}; + MyHydratedCubit().onChange(transition); + verify(() => storage.write('MyHydratedCubit', expected)).called(2); + }, storage: storage); }); test('should call storage.write when onChange is called with cubit id', () { - final cubit = MyHydratedCubit('A'); - final transition = const Change( - currentState: 0, - nextState: 0, - ); - final expected = {'value': 0}; - cubit.onChange(transition); - verify(() => storage.write('MyHydratedCubitA', expected)).called(2); + HydratedBlocOverrides.runZoned(() { + final cubit = MyHydratedCubit('A'); + final transition = const Change( + currentState: 0, + nextState: 0, + ); + final expected = {'value': 0}; + cubit.onChange(transition); + verify(() => storage.write('MyHydratedCubitA', expected)).called(2); + }, storage: storage); }); test('should throw BlocUnhandledErrorException when storage.write throws', () { - runZonedGuarded( - () async { - final expectedError = Exception('oops'); - final transition = const Change( - currentState: 0, - nextState: 0, - ); - when( - () => storage.write(any(), any()), - ).thenThrow(expectedError); - MyHydratedCubit().onChange(transition); - await Future.delayed(const Duration(seconds: 300)); - fail('should throw'); - }, - (error, _) { - expect( - (error as BlocUnhandledErrorException).error.toString(), - 'Exception: oops', - ); - expect(error.stackTrace, isNotNull); - }, - ); + HydratedBlocOverrides.runZoned(() { + runZonedGuarded( + () async { + final expectedError = Exception('oops'); + final transition = const Change( + currentState: 0, + nextState: 0, + ); + when( + () => storage.write(any(), any()), + ).thenThrow(expectedError); + MyHydratedCubit().onChange(transition); + await Future.delayed(const Duration(seconds: 300)); + fail('should throw'); + }, + (error, _) { + expect(error.toString(), 'Exception: oops'); + }, + ); + }, storage: storage); }); test('stores initial state when instantiated', () { - MyHydratedCubit(); - verify(() => storage.write('MyHydratedCubit', {'value': 0})).called(1); + HydratedBlocOverrides.runZoned(() { + MyHydratedCubit(); + verify( + () => storage.write('MyHydratedCubit', {'value': 0}), + ).called(1); + }, storage: storage); }); test('initial state should return 0 when fromJson returns null', () { - when(() => storage.read(any())).thenReturn(null); - expect(MyHydratedCubit().state, 0); - verify(() => storage.read('MyHydratedCubit')).called(1); + HydratedBlocOverrides.runZoned(() { + when(() => storage.read(any())).thenReturn(null); + expect(MyHydratedCubit().state, 0); + verify(() => storage.read('MyHydratedCubit')).called(1); + }, storage: storage); }); test('initial state should return 0 when deserialization fails', () { - when(() => storage.read(any())).thenThrow(Exception('oops')); - expect(MyHydratedCubit('', false).state, 0); + HydratedBlocOverrides.runZoned(() { + when(() => storage.read(any())).thenThrow(Exception('oops')); + expect(MyHydratedCubit('', false).state, 0); + }, storage: storage); }); test('initial state should return 101 when fromJson returns 101', () { - when(() => storage.read(any())).thenReturn({'value': 101}); - expect(MyHydratedCubit().state, 101); - verify(() => storage.read('MyHydratedCubit')).called(1); + HydratedBlocOverrides.runZoned(() { + when(() => storage.read(any())).thenReturn({'value': 101}); + expect(MyHydratedCubit().state, 101); + verify(() => storage.read('MyHydratedCubit')).called(1); + }, storage: storage); }); group('clear', () { test('calls delete on storage', () async { - await MyHydratedCubit().clear(); - verify(() => storage.delete('MyHydratedCubit')).called(1); + await HydratedBlocOverrides.runZoned(() async { + await MyHydratedCubit().clear(); + verify(() => storage.delete('MyHydratedCubit')).called(1); + }, storage: storage); }); }); }); group('MultiHydratedCubit', () { test('initial state should return 0 when fromJson returns null', () { - when(() => storage.read(any())).thenReturn(null); - expect(MyMultiHydratedCubit('A').state, 0); - verify(() => storage.read('MyMultiHydratedCubitA')).called(1); - - expect(MyMultiHydratedCubit('B').state, 0); - verify(() => storage.read('MyMultiHydratedCubitB')).called(1); + HydratedBlocOverrides.runZoned(() { + when(() => storage.read(any())).thenReturn(null); + expect(MyMultiHydratedCubit('A').state, 0); + verify( + () => storage.read('MyMultiHydratedCubitA'), + ).called(1); + + expect(MyMultiHydratedCubit('B').state, 0); + verify( + () => storage.read('MyMultiHydratedCubitB'), + ).called(1); + }, storage: storage); }); test('initial state should return 101/102 when fromJson returns 101/102', () { - when( - () => storage.read('MyMultiHydratedCubitA'), - ).thenReturn({'value': 101}); - expect(MyMultiHydratedCubit('A').state, 101); - verify(() => storage.read('MyMultiHydratedCubitA')).called(1); - - when( - () => storage.read('MyMultiHydratedCubitB'), - ).thenReturn({'value': 102}); - expect(MyMultiHydratedCubit('B').state, 102); - verify(() => storage.read('MyMultiHydratedCubitB')).called(1); + HydratedBlocOverrides.runZoned(() { + when( + () => storage.read('MyMultiHydratedCubitA'), + ).thenReturn({'value': 101}); + expect(MyMultiHydratedCubit('A').state, 101); + verify( + () => storage.read('MyMultiHydratedCubitA'), + ).called(1); + + when( + () => storage.read('MyMultiHydratedCubitB'), + ).thenReturn({'value': 102}); + expect(MyMultiHydratedCubit('B').state, 102); + verify( + () => storage.read('MyMultiHydratedCubitB'), + ).called(1); + }, storage: storage); }); group('clear', () { test('calls delete on storage', () async { - await MyMultiHydratedCubit('A').clear(); - verify(() => storage.delete('MyMultiHydratedCubitA')).called(1); - verifyNever(() => storage.delete('MyMultiHydratedCubitB')); - - await MyMultiHydratedCubit('B').clear(); - verify(() => storage.delete('MyMultiHydratedCubitB')).called(1); + await HydratedBlocOverrides.runZoned(() async { + await MyMultiHydratedCubit('A').clear(); + verify(() => storage.delete('MyMultiHydratedCubitA')).called(1); + verifyNever(() => storage.delete('MyMultiHydratedCubitB')); + + await MyMultiHydratedCubit('B').clear(); + verify(() => storage.delete('MyMultiHydratedCubitB')).called(1); + }, storage: storage); }); }); }); group('MyUuidHydratedCubit', () { test('stores initial state when instantiated', () { - MyUuidHydratedCubit(); - verify( - () => storage.write('MyUuidHydratedCubit', any()), - ).called(1); + HydratedBlocOverrides.runZoned(() { + MyUuidHydratedCubit(); + verify( + () => storage.write('MyUuidHydratedCubit', any()), + ).called(1); + }, storage: storage); }); - test('correctly caches computed initial state', () async { - dynamic cachedState; - when(() => storage.read(any())).thenReturn(cachedState); - when( - () => storage.write(any(), any()), - ).thenAnswer((_) => Future.value()); - MyUuidHydratedCubit(); - final captured = verify( - () => storage.write('MyUuidHydratedCubit', captureAny()), - ).captured; - cachedState = captured.first; - when(() => storage.read(any())).thenReturn(cachedState); - MyUuidHydratedCubit(); - final secondCaptured = verify( - () => storage.write('MyUuidHydratedCubit', captureAny()), - ).captured; - final dynamic initialStateB = secondCaptured.first; - - expect(initialStateB, cachedState); + test('correctly caches computed initial state', () { + HydratedBlocOverrides.runZoned(() { + dynamic cachedState; + when(() => storage.read(any())).thenReturn(cachedState); + when( + () => storage.write(any(), any()), + ).thenAnswer((_) => Future.value()); + MyUuidHydratedCubit(); + final captured = verify( + () => storage.write('MyUuidHydratedCubit', captureAny()), + ).captured; + cachedState = captured.first; + when(() => storage.read(any())).thenReturn(cachedState); + MyUuidHydratedCubit(); + final secondCaptured = verify( + () => storage.write('MyUuidHydratedCubit', captureAny()), + ).captured; + final dynamic initialStateB = secondCaptured.first; + + expect(initialStateB, cachedState); + }, storage: storage); }); }); }); diff --git a/packages/hydrated_bloc/test/hydrated_storage_test.dart b/packages/hydrated_bloc/test/hydrated_storage_test.dart index 6b0f917d540..c1bdb42b997 100644 --- a/packages/hydrated_bloc/test/hydrated_storage_test.dart +++ b/packages/hydrated_bloc/test/hydrated_storage_test.dart @@ -8,12 +8,20 @@ import 'package:hive/src/hive_impl.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; -import 'package:pedantic/pedantic.dart'; import 'package:test/test.dart'; class MockBox extends Mock implements Box {} void main() { + group('DefaultStorage', () { + test('throws NoSuchMethodError', () { + HydratedBlocOverrides.runZoned(() { + final overrides = HydratedBlocOverrides.current!; + expect(() => overrides.storage.read(''), throwsNoSuchMethodError); + }); + }); + }); + group('HydratedStorage', () { final cwd = Directory.current.absolute.path; final storageDirectory = Directory(cwd); diff --git a/packages/hydrated_bloc/test/legacy/hydrated_bloc_test.dart b/packages/hydrated_bloc/test/legacy/hydrated_bloc_test.dart deleted file mode 100644 index 2d60950de9d..00000000000 --- a/packages/hydrated_bloc/test/legacy/hydrated_bloc_test.dart +++ /dev/null @@ -1,475 +0,0 @@ -// ignore_for_file: invalid_use_of_protected_member -import 'dart:async'; - -import 'package:hydrated_bloc/hydrated_bloc.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pedantic/pedantic.dart'; -import 'package:test/test.dart'; -import 'package:uuid/uuid.dart'; - -class MockStorage extends Mock implements Storage {} - -class MyUuidHydratedBloc extends HydratedBloc { - MyUuidHydratedBloc() : super(const Uuid().v4()); - - @override - Stream mapEventToState(String event) async* {} - - @override - Map toJson(String? state) => {'value': state}; - - @override - String? fromJson(dynamic json) { - try { - return json['value'] as String; - } catch (_) { - // ignore: avoid_returning_null - return null; - } - } -} - -enum CounterEvent { increment } - -class MyCallbackHydratedBloc extends HydratedBloc { - MyCallbackHydratedBloc({this.onFromJsonCalled}) : super(0); - - final void Function(dynamic)? onFromJsonCalled; - - @override - Stream mapEventToState(CounterEvent event) async* { - switch (event) { - case CounterEvent.increment: - yield state + 1; - break; - } - } - - @override - Map toJson(int state) => {'value': state}; - - @override - int? fromJson(Map json) { - onFromJsonCalled?.call(json); - return json['value'] as int?; - } -} - -class MyHydratedBloc extends HydratedBloc { - MyHydratedBloc([this._id]) : super(0); - - final String? _id; - - @override - String get id => _id ?? ''; - - @override - Stream mapEventToState(int event) async* {} - - @override - Map? toJson(int state) { - return {'value': state}; - } - - @override - int? fromJson(Map json) => json['value'] as int?; -} - -class MyMultiHydratedBloc extends HydratedBloc { - MyMultiHydratedBloc(String id) - : _id = id, - super(0); - - final String _id; - - @override - String get id => _id; - - @override - Stream mapEventToState(int event) async* {} - - @override - Map toJson(int state) { - return {'value': state}; - } - - @override - int? fromJson(dynamic json) => json['value'] as int?; -} - -class MyErrorThrowingBloc extends HydratedBloc { - MyErrorThrowingBloc({this.onErrorCallback, this.superOnError = true}) - : super(0); - - final Function(Object error, StackTrace stackTrace)? onErrorCallback; - final bool superOnError; - - @override - Stream mapEventToState(Object event) async* { - yield state + 1; - } - - @override - void onError(Object error, StackTrace stackTrace) { - onErrorCallback?.call(error, stackTrace); - if (superOnError) { - super.onError(error, stackTrace); - } - } - - @override - Map toJson(int state) { - return {'key': Object}; - } - - @override - int fromJson(dynamic json) { - return 0; - } -} - -void main() { - group('HydratedBloc', () { - late Storage storage; - - setUpAll(() { - registerFallbackValue(StackTrace.empty); - registerFallbackValue(const {}); - }); - - setUp(() { - storage = MockStorage(); - when(() => storage.read(any())).thenReturn({}); - when(() => storage.write(any(), any())).thenAnswer((_) async {}); - when(() => storage.delete(any())).thenAnswer((_) async {}); - when(() => storage.clear()).thenAnswer((_) async {}); - HydratedBloc.storage = storage; - }); - - test('storage getter returns correct storage instance', () { - final storage = MockStorage(); - HydratedBloc.storage = storage; - expect(HydratedBloc.storage, storage); - }); - - test('reads from storage once upon initialization', () { - MyCallbackHydratedBloc(); - verify(() => storage.read('MyCallbackHydratedBloc')).called(1); - }); - - test( - 'does not read from storage on subsequent state changes ' - 'when cache value exists', () async { - when(() => storage.read(any())).thenReturn({'value': 42}); - final bloc = MyCallbackHydratedBloc(); - expect(bloc.state, 42); - bloc.add(CounterEvent.increment); - await expectLater(bloc.stream, emitsInOrder(const [43])); - verify(() => storage.read('MyCallbackHydratedBloc')).called(1); - }); - - test( - 'does not deserialize state on subsequent state changes ' - 'when cache value exists', () async { - final fromJsonCalls = []; - when(() => storage.read(any())).thenReturn({'value': 42}); - final bloc = MyCallbackHydratedBloc( - onFromJsonCalled: fromJsonCalls.add, - ); - expect(bloc.state, 42); - bloc.add(CounterEvent.increment); - await expectLater(bloc.stream, emitsInOrder(const [43])); - expect(fromJsonCalls, [ - {'value': 42} - ]); - }); - - test( - 'does not read from storage on subsequent state changes ' - 'when cache is empty', () async { - when(() => storage.read(any())).thenReturn(null); - final bloc = MyCallbackHydratedBloc(); - expect(bloc.state, 0); - bloc.add(CounterEvent.increment); - await expectLater(bloc.stream, emitsInOrder(const [1])); - verify(() => storage.read('MyCallbackHydratedBloc')).called(1); - }); - - test('does not deserialize state when cache is empty', () async { - final fromJsonCalls = []; - when(() => storage.read(any())).thenReturn(null); - final bloc = MyCallbackHydratedBloc( - onFromJsonCalled: fromJsonCalls.add, - ); - expect(bloc.state, 0); - bloc.add(CounterEvent.increment); - await expectLater(bloc.stream, emitsInOrder(const [1])); - expect(fromJsonCalls, isEmpty); - }); - - test( - 'does not read from storage on subsequent state changes ' - 'when cache is malformed', () async { - unawaited(runZonedGuarded(() async { - when(() => storage.read(any())).thenReturn('{'); - MyCallbackHydratedBloc().add(CounterEvent.increment); - }, (_, __) { - verify(() => storage.read('MyCallbackHydratedBloc')).called(1); - })); - }); - - test('does not deserialize state when cache is malformed', () async { - final fromJsonCalls = []; - unawaited(runZonedGuarded(() async { - when(() => storage.read(any())).thenReturn('{'); - MyCallbackHydratedBloc( - onFromJsonCalled: fromJsonCalls.add, - ).add(CounterEvent.increment); - expect(fromJsonCalls, isEmpty); - }, (_, __) { - expect(fromJsonCalls, isEmpty); - })); - }); - - group('SingleHydratedBloc', () { - test('should call storage.write when onChange is called', () { - const change = Change( - currentState: 0, - nextState: 0, - ); - const expected = {'value': 0}; - MyHydratedBloc().onChange(change); - verify(() => storage.write('MyHydratedBloc', expected)).called(2); - }); - - test('should call storage.write when onChange is called with bloc id', - () { - final bloc = MyHydratedBloc('A'); - const change = Change( - currentState: 0, - nextState: 0, - ); - const expected = {'value': 0}; - bloc.onChange(change); - verify(() => storage.write('MyHydratedBlocA', expected)).called(2); - }); - - test('should call onError when storage.write throws', () { - runZonedGuarded(() async { - final expectedError = Exception('oops'); - const change = Change( - currentState: 0, - nextState: 0, - ); - final bloc = MyHydratedBloc(); - when( - () => storage.write(any(), any()), - ).thenThrow(expectedError); - bloc.onChange(change); - await Future.delayed(const Duration(milliseconds: 300)); - verify(() => bloc.onError(expectedError, any())).called(2); - }, (error, _) { - expect( - (error as BlocUnhandledErrorException).error.toString(), - 'Exception: oops', - ); - expect((error).stackTrace, isNotNull); - }); - }); - - test('stores initial state when instantiated', () { - MyHydratedBloc(); - verify( - () => storage.write('MyHydratedBloc', {'value': 0}), - ).called(1); - }); - - test('initial state should return 0 when fromJson returns null', () { - when(() => storage.read(any())).thenReturn(null); - expect(MyHydratedBloc().state, 0); - verify(() => storage.read('MyHydratedBloc')).called(1); - }); - - test('initial state should return 101 when fromJson returns 101', () { - when(() => storage.read(any())).thenReturn({'value': 101}); - expect(MyHydratedBloc().state, 101); - verify(() => storage.read('MyHydratedBloc')).called(1); - }); - - group('clear', () { - test('calls delete on storage', () async { - await MyHydratedBloc().clear(); - verify(() => storage.delete('MyHydratedBloc')).called(1); - }); - }); - }); - - group('MultiHydratedBloc', () { - test('initial state should return 0 when fromJson returns null', () { - when(() => storage.read(any())).thenReturn(null); - expect(MyMultiHydratedBloc('A').state, 0); - verify(() => storage.read('MyMultiHydratedBlocA')).called(1); - - expect(MyMultiHydratedBloc('B').state, 0); - verify(() => storage.read('MyMultiHydratedBlocB')).called(1); - }); - - test('initial state should return 101/102 when fromJson returns 101/102', - () { - when( - () => storage.read('MyMultiHydratedBlocA'), - ).thenReturn({'value': 101}); - expect(MyMultiHydratedBloc('A').state, 101); - verify(() => storage.read('MyMultiHydratedBlocA')).called(1); - - when( - () => storage.read('MyMultiHydratedBlocB'), - ).thenReturn({'value': 102}); - expect(MyMultiHydratedBloc('B').state, 102); - verify(() => storage.read('MyMultiHydratedBlocB')).called(1); - }); - - group('clear', () { - test('calls delete on storage', () async { - await MyMultiHydratedBloc('A').clear(); - verify(() => storage.delete('MyMultiHydratedBlocA')).called(1); - verifyNever(() => storage.delete('MyMultiHydratedBlocB')); - - await MyMultiHydratedBloc('B').clear(); - verify(() => storage.delete('MyMultiHydratedBlocB')).called(1); - }); - }); - }); - - group('MyUuidHydratedBloc', () { - test('stores initial state when instantiated', () async { - when( - () => storage.write(any(), any>()), - ).thenAnswer((_) async {}); - MyUuidHydratedBloc(); - await untilCalled( - () => storage.write(any(), any>()), - ); - verify( - () => storage.write( - 'MyUuidHydratedBloc', - any>(), - ), - ).called(1); - }); - - test('correctly caches computed initial state', () async { - dynamic cachedState; - when(() => storage.read(any())).thenReturn(cachedState); - when( - () => storage.write(any(), any()), - ).thenAnswer((_) => Future.value()); - MyUuidHydratedBloc(); - final captured = verify( - () => storage.write('MyUuidHydratedBloc', captureAny()), - ).captured; - cachedState = captured.first; - when(() => storage.read(any())).thenReturn(cachedState); - MyUuidHydratedBloc(); - final secondCaptured = verify( - () => storage.write('MyUuidHydratedBloc', captureAny()), - ).captured; - final dynamic initialStateB = secondCaptured.first; - - expect(initialStateB, cachedState); - }); - }); - - group('MyErrorThrowingBloc', () { - test('continues to emit new states when serialization fails', () async { - unawaited(runZonedGuarded( - () async { - final bloc = MyErrorThrowingBloc(); - final expectedStates = [0, 1, emitsDone]; - unawaited(expectLater(bloc.stream, emitsInOrder(expectedStates))); - bloc.add(Object); - await bloc.close(); - }, - (_, __) {}, - )); - }); - - test('calls onError when json decode fails', () async { - Object? lastError; - StackTrace? lastStackTrace; - unawaited(runZonedGuarded(() async { - when(() => storage.read(any())).thenReturn('invalid json'); - MyErrorThrowingBloc( - onErrorCallback: (error, stackTrace) { - lastError = error; - lastStackTrace = stackTrace; - }, - ); - }, (_, __) { - expect(lastStackTrace, isNotNull); - expect( - lastError.toString().startsWith( - '''Unhandled error type \'String\' is not a subtype of type \'Map?\' in type cast''', - ), - isTrue, - ); - })); - }); - - test('returns super.state when json decode fails', () async { - MyErrorThrowingBloc? bloc; - unawaited(runZonedGuarded(() async { - when(() => storage.read(any())).thenReturn('invalid json'); - bloc = MyErrorThrowingBloc(superOnError: false); - }, (_, __) { - expect(bloc?.state, 0); - })); - }); - - test('calls onError when storage.write fails', () async { - Object? lastError; - StackTrace? lastStackTrace; - final exception = Exception('oops'); - unawaited(runZonedGuarded(() async { - when(() => storage.write(any(), any())).thenThrow(exception); - MyErrorThrowingBloc( - onErrorCallback: (error, stackTrace) { - lastError = error; - lastStackTrace = stackTrace; - }, - ); - }, (error, _) { - expect(lastError, isA()); - expect(lastStackTrace, isNotNull); - expect( - (error as BlocUnhandledErrorException).error.toString(), - '''Converting object to an encodable object failed: Object''', - ); - expect(error.stackTrace, isNotNull); - })); - }); - - test('calls onError when json encode fails', () async { - unawaited(runZonedGuarded( - () async { - Object? lastError; - StackTrace? lastStackTrace; - final bloc = MyErrorThrowingBloc( - onErrorCallback: (error, stackTrace) { - lastError = error; - lastStackTrace = stackTrace; - }, - )..add(Object); - await bloc.close(); - expect( - '$lastError', - 'Converting object to an encodable object failed: Object', - ); - expect(lastStackTrace, isNotNull); - }, - (_, __) {}, - )); - }); - }); - }); -} diff --git a/packages/replay_bloc/CHANGELOG.md b/packages/replay_bloc/CHANGELOG.md index 057b6bbdbec..2467b9564dd 100644 --- a/packages/replay_bloc/CHANGELOG.md +++ b/packages/replay_bloc/CHANGELOG.md @@ -1,3 +1,15 @@ +# 0.2.0-dev.3 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.5` + +# 0.2.0-dev.2 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.3` + +# 0.2.0-dev.1 + +- **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.2` + # 0.1.0 - feat: upgrade to `bloc ^7.2.0` diff --git a/packages/replay_bloc/example/lib/main.dart b/packages/replay_bloc/example/lib/main.dart index a2c10cfc38d..241c762a464 100644 --- a/packages/replay_bloc/example/lib/main.dart +++ b/packages/replay_bloc/example/lib/main.dart @@ -1,32 +1,57 @@ -import 'package:example/simple_bloc_observer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:replay_bloc/replay_bloc.dart'; -void main() async { - Bloc.observer = SimpleBlocObserver(); - runApp(App()); +void main() { + BlocOverrides.runZoned( + () => runApp(const App()), + blocObserver: AppBlocObserver(), + ); } -/// A [StatelessWidget] which uses: -/// * [replay_bloc](https://pub.dev/packages/replay_bloc) -/// * [flutter_bloc](https://pub.dev/packages/flutter_bloc) +/// Custom [BlocObserver] that observes all bloc and cubit state changes. +class AppBlocObserver extends BlocObserver { + @override + void onChange(BlocBase bloc, Change change) { + super.onChange(bloc, change); + if (bloc is Cubit) print(change); + } + + @override + void onTransition(Bloc bloc, Transition transition) { + super.onTransition(bloc, transition); + print(transition); + } +} + +/// {@template app} +/// A [StatelessWidget] that: +/// * uses [replay_bloc](https://pub.dev/packages/replay_bloc) +/// and [flutter_bloc](https://pub.dev/packages/flutter_bloc) /// to manage the state of a counter. +/// {@endtemplate} class App extends StatelessWidget { + /// {@macro app} + const App({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return BlocProvider( create: (_) => CounterBloc(), - child: MaterialApp( + child: const MaterialApp( home: CounterPage(), ), ); } } -/// A [StatelessWidget] which demonstrates -/// how to consume and interact with a [ReplayBloc] or [ReplayCubit]. +/// {@template counter_page} +/// A [StatelessWidget] that: +/// * demonstrates how to consume and interact with a [ReplayBloc]/[ReplayCubit]. +/// {@endtemplate} class CounterPage extends StatelessWidget { + /// {@macro counter_page} + const CounterPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; @@ -65,26 +90,19 @@ class CounterPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end, children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () => context.read().add(Increment()), - ), + FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () => context.read().add(Increment()), ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: FloatingActionButton( - child: const Icon(Icons.remove), - onPressed: () => context.read().add(Decrement()), - ), + const SizedBox(height: 4), + FloatingActionButton( + child: const Icon(Icons.remove), + onPressed: () => context.read().add(Decrement()), ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: FloatingActionButton( - child: const Icon(Icons.delete_forever), - onPressed: () => context.read().add(Reset()), - ), + const SizedBox(height: 4), + FloatingActionButton( + child: const Icon(Icons.delete_forever), + onPressed: () => context.read().add(Reset()), ), ], ), diff --git a/packages/replay_bloc/example/lib/simple_bloc_observer.dart b/packages/replay_bloc/example/lib/simple_bloc_observer.dart deleted file mode 100644 index fbc70ba8017..00000000000 --- a/packages/replay_bloc/example/lib/simple_bloc_observer.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:bloc/bloc.dart'; - -/// Simple [BlocObserver] which just prints to the debug console. -class SimpleBlocObserver extends BlocObserver { - @override - void onEvent(Bloc bloc, Object? event) { - super.onEvent(bloc, event); - print('${bloc.runtimeType} $event'); - } - - @override - void onError(BlocBase bloc, Object error, StackTrace stackTrace) { - print('${bloc.runtimeType} $error $stackTrace'); - super.onError(bloc, error, stackTrace); - } - - @override - void onChange(BlocBase bloc, Change change) { - super.onChange(bloc, change); - print('${bloc.runtimeType} $change'); - } - - @override - void onTransition(Bloc bloc, Transition transition) { - super.onTransition(bloc, transition); - print('${bloc.runtimeType} $transition'); - } -} diff --git a/packages/replay_bloc/lib/replay_bloc.dart b/packages/replay_bloc/lib/replay_bloc.dart index 057d4111cb2..ffd52f0836c 100644 --- a/packages/replay_bloc/lib/replay_bloc.dart +++ b/packages/replay_bloc/lib/replay_bloc.dart @@ -5,5 +5,4 @@ library replay_bloc; export 'package:bloc/bloc.dart'; - export 'src/replay_cubit.dart'; diff --git a/packages/replay_bloc/lib/src/replay_bloc.dart b/packages/replay_bloc/lib/src/replay_bloc.dart index a9465a52220..7cc395b20c4 100644 --- a/packages/replay_bloc/lib/src/replay_bloc.dart +++ b/packages/replay_bloc/lib/src/replay_bloc.dart @@ -71,6 +71,7 @@ abstract class ReplayBloc /// A mixin which enables `undo` and `redo` operations /// for [Bloc] classes. mixin ReplayBlocMixin on Bloc { + late final _blocObserver = BlocOverrides.current?.blocObserver; late final _changeStack = _ChangeStack(shouldReplay: shouldReplay); /// Sets the internal `undo`/`redo` size limit. @@ -81,14 +82,14 @@ mixin ReplayBlocMixin on Bloc { // ignore: must_call_super void onTransition(covariant Transition transition) { // ignore: invalid_use_of_protected_member - Bloc.observer.onTransition(this, transition); + _blocObserver?.onTransition(this, transition); } @override // ignore: must_call_super void onEvent(covariant ReplayEvent event) { // ignore: invalid_use_of_protected_member - Bloc.observer.onEvent(this, event); + _blocObserver?.onEvent(this, event); } @override @@ -104,7 +105,7 @@ mixin ReplayBlocMixin on Bloc { event: event, nextState: state, )); - // ignore: invalid_use_of_visible_for_testing_member + // ignore: invalid_use_of_internal_member super.emit(state); }, (val) { @@ -115,11 +116,11 @@ mixin ReplayBlocMixin on Bloc { event: event, nextState: val, )); - // ignore: invalid_use_of_visible_for_testing_member + // ignore: invalid_use_of_internal_member super.emit(val); }, )); - // ignore: invalid_use_of_visible_for_testing_member + // ignore: invalid_use_of_internal_member super.emit(state); } diff --git a/packages/replay_bloc/pubspec.yaml b/packages/replay_bloc/pubspec.yaml index 7ae78f62354..808074571dd 100644 --- a/packages/replay_bloc/pubspec.yaml +++ b/packages/replay_bloc/pubspec.yaml @@ -1,15 +1,15 @@ name: replay_bloc description: An extension to the bloc state management library which adds support for undo and redo. -version: 0.1.0 -repository: https://github.com/felangel/bloc +version: 0.2.0-dev.3 +repository: https://github.com/felangel/bloc/tree/master/packages/replay_bloc issue_tracker: https://github.com/felangel/bloc/issues -homepage: https://github.com/felangel/bloc +homepage: https://bloclibrary.dev environment: sdk: ">=2.12.0 <3.0.0" dependencies: - bloc: ^7.2.0 + bloc: ^8.0.0-dev.5 dev_dependencies: test: ^1.16.0 diff --git a/packages/replay_bloc/test/legacy/blocs/counter_bloc.dart b/packages/replay_bloc/test/legacy/blocs/counter_bloc.dart deleted file mode 100644 index e8a42e230bb..00000000000 --- a/packages/replay_bloc/test/legacy/blocs/counter_bloc.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:replay_bloc/replay_bloc.dart'; - -abstract class CounterEvent extends ReplayEvent {} - -class Increment extends CounterEvent {} - -class Decrement extends CounterEvent {} - -class CounterBloc extends ReplayBloc { - CounterBloc({ - int? limit, - this.onEventCallback, - this.onTransitionCallback, - this.shouldReplayCallback, - }) : super(0, limit: limit); - - final void Function(ReplayEvent)? onEventCallback; - final void Function(Transition)? onTransitionCallback; - final bool Function(int)? shouldReplayCallback; - - @override - Stream mapEventToState(CounterEvent event) async* { - if (event is Increment) { - yield state + 1; - } else if (event is Decrement) { - yield state - 1; - } - } - - @override - void onEvent(ReplayEvent event) { - onEventCallback?.call(event); - super.onEvent(event); - } - - @override - void onTransition(Transition transition) { - onTransitionCallback?.call(transition); - super.onTransition(transition); - } - - @override - bool shouldReplay(int state) { - return shouldReplayCallback?.call(state) ?? super.shouldReplay(state); - } -} - -class CounterBlocMixin extends Bloc - with ReplayBlocMixin { - CounterBlocMixin({int? limit}) : super(0) { - if (limit != null) { - this.limit = limit; - } - } - - @override - Stream mapEventToState(CounterEvent event) async* { - if (event is Increment) { - yield state + 1; - } else if (event is Decrement) { - yield state - 1; - } - } -} diff --git a/packages/replay_bloc/test/legacy/replay_bloc_test.dart b/packages/replay_bloc/test/legacy/replay_bloc_test.dart deleted file mode 100644 index a44a1a7d84c..00000000000 --- a/packages/replay_bloc/test/legacy/replay_bloc_test.dart +++ /dev/null @@ -1,881 +0,0 @@ -import 'dart:async'; - -import 'package:replay_bloc/replay_bloc.dart'; -import 'package:test/test.dart'; - -import 'blocs/counter_bloc.dart'; - -void main() { - group('ReplayBloc', () { - group('initial state', () { - test('is correct', () { - expect(CounterBloc().state, 0); - }); - }); - - group('canUndo', () { - test('is false when no state changes have occurred', () async { - final bloc = CounterBloc(); - expect(bloc.canUndo, isFalse); - await bloc.close(); - }); - - test('is true when a single state change has occurred', () async { - final bloc = CounterBloc()..add(Increment()); - await Future.delayed(Duration.zero); - expect(bloc.canUndo, isTrue); - await bloc.close(); - }); - - test('is false when undos have been exhausted', () async { - final bloc = CounterBloc()..add(Increment()); - await Future.delayed(Duration.zero, bloc.undo); - expect(bloc.canUndo, isFalse); - await bloc.close(); - }); - }); - - group('canRedo', () { - test('is false when no state changes have occurred', () async { - final bloc = CounterBloc(); - expect(bloc.canRedo, isFalse); - await bloc.close(); - }); - - test('is true when a single undo has occurred', () async { - final bloc = CounterBloc()..add(Increment()); - await Future.delayed(Duration.zero, bloc.undo); - expect(bloc.canRedo, isTrue); - await bloc.close(); - }); - - test('is false when redos have been exhausted', () async { - final bloc = CounterBloc()..add(Increment()); - await Future.delayed(Duration.zero, bloc.undo); - await Future.delayed(Duration.zero, bloc.redo); - expect(bloc.canRedo, isFalse); - await bloc.close(); - }); - }); - - group('clearHistory', () { - test('clears history and redos on new bloc', () async { - final bloc = CounterBloc()..clearHistory(); - expect(bloc.canRedo, isFalse); - expect(bloc.canUndo, isFalse); - await bloc.close(); - }); - }); - - group('undo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBloc(); - final subscription = bloc.stream.listen(states.add); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when limit is 0', () async { - final states = []; - final bloc = CounterBloc(limit: 0); - final subscription = bloc.stream.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1]); - }); - - test('skips states filtered out by shouldReplay at undo time', () async { - final states = []; - final bloc = CounterBloc(shouldReplayCallback: (i) => !i.isEven); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..undo() - ..undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 3, 1]); - }); - - test( - 'doesn\'t skip states that would be filtered out by shouldReplay ' - 'at transition time but not at undo time', () async { - var replayEvens = false; - final states = []; - final bloc = CounterBloc( - shouldReplayCallback: (i) => !i.isEven || replayEvens, - ); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - replayEvens = true; - bloc - ..undo() - ..undo() - ..undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 3, 2, 1, 0]); - }); - - test('loses history outside of limit', () async { - final states = []; - final bloc = CounterBloc(limit: 1); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - - test('reverts to initial state', () async { - final states = []; - final bloc = CounterBloc(); - final subscription = bloc.stream.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 0]); - }); - - test('reverts to previous state with multiple state changes ', () async { - final states = []; - final bloc = CounterBloc(); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - - test('triggers onEvent', () async { - final onEventCalls = []; - final bloc = CounterBloc(onEventCallback: onEventCalls.add) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - expect(onEventCalls.length, 2); - expect(onEventCalls.last.toString(), 'Undo'); - }); - - test('triggers onTransition', () async { - final onTransitionCalls = >[]; - final bloc = CounterBloc(onTransitionCallback: onTransitionCalls.add) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - expect(onTransitionCalls.length, 2); - expect( - onTransitionCalls.last.toString(), - 'Transition { currentState: 1, event: Undo, nextState: 0 }', - ); - }); - }); - - group('redo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBloc(); - final subscription = bloc.stream.listen(states.add); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when no undos have occurred', () async { - final states = []; - final bloc = CounterBloc(); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2]); - }); - - test('works when one undo has occurred', () async { - final states = []; - final bloc = CounterBloc(); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test('triggers onEvent', () async { - final onEventCalls = []; - final bloc = CounterBloc(onEventCallback: onEventCalls.add) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo(); - await bloc.close(); - expect(onEventCalls.length, 3); - expect(onEventCalls.last.toString(), 'Redo'); - }); - - test('triggers onTransition', () async { - final onTransitionCalls = >[]; - final bloc = CounterBloc(onTransitionCallback: onTransitionCalls.add) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo(); - await bloc.close(); - expect(onTransitionCalls.length, 3); - expect( - onTransitionCalls.last.toString(), - 'Transition { currentState: 0, event: Redo, nextState: 1 }', - ); - }); - - test('does nothing when undos have been exhausted', () async { - final states = []; - final bloc = CounterBloc(); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test( - 'does nothing when undos has occurred ' - 'followed by a new state change', () async { - final states = []; - final bloc = CounterBloc(); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..add(Decrement()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 0]); - }); - - test( - 'redo does not redo states which were' - ' filtered out by shouldReplay at undo time', () async { - final states = []; - final bloc = CounterBloc(shouldReplayCallback: (i) => !i.isEven); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..undo() - ..undo() - ..redo() - ..redo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 3, 1, 3]); - }); - - test( - 'redo does not redo states which were' - ' filtered out by shouldReplay at transition time', () async { - var replayEvens = false; - final states = []; - final bloc = CounterBloc( - shouldReplayCallback: (i) => !i.isEven || replayEvens, - ); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..undo() - ..undo(); - replayEvens = true; - bloc - ..redo() - ..redo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 3, 1, 2, 3]); - }); - }); - }); - - group('ReplayBlocMixin', () { - group('initial state', () { - test('is correct', () { - expect(CounterBlocMixin().state, 0); - }); - }); - - group('canUndo', () { - test('is false when no state changes have occurred', () async { - final bloc = CounterBlocMixin(); - expect(bloc.canUndo, isFalse); - await bloc.close(); - }); - - test('is true when a single state change has occurred', () async { - final bloc = CounterBlocMixin()..add(Increment()); - await Future.delayed(Duration.zero); - expect(bloc.canUndo, isTrue); - await bloc.close(); - }); - - test('is false when undos have been exhausted', () async { - final bloc = CounterBlocMixin()..add(Increment()); - await Future.delayed(Duration.zero, bloc.undo); - expect(bloc.canUndo, isFalse); - await bloc.close(); - }); - }); - - group('canRedo', () { - test('is false when no state changes have occurred', () async { - final bloc = CounterBlocMixin(); - expect(bloc.canRedo, isFalse); - await bloc.close(); - }); - - test('is true when a single undo has occurred', () async { - final bloc = CounterBlocMixin()..add(Increment()); - await Future.delayed(Duration.zero, bloc.undo); - expect(bloc.canRedo, isTrue); - await bloc.close(); - }); - - test('is false when redos have been exhausted', () async { - final bloc = CounterBlocMixin()..add(Increment()); - await Future.delayed(Duration.zero, bloc.undo); - await Future.delayed(Duration.zero, bloc.redo); - expect(bloc.canRedo, isFalse); - await bloc.close(); - }); - }); - - group('clearHistory', () { - test('clears history and redos on new bloc', () async { - final bloc = CounterBlocMixin()..clearHistory(); - expect(bloc.canRedo, isFalse); - expect(bloc.canUndo, isFalse); - await bloc.close(); - }); - }); - - group('undo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - final subscription = bloc.stream.listen(states.add); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when limit is 0', () async { - final states = []; - final bloc = CounterBlocMixin(limit: 0); - final subscription = bloc.stream.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1]); - }); - - test('loses history outside of limit', () async { - final states = []; - final bloc = CounterBlocMixin(limit: 1); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - - test('reverts to initial state', () async { - final states = []; - final bloc = CounterBlocMixin(); - final subscription = bloc.stream.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 0]); - }); - - test('reverts to previous state with multiple state changes ', () async { - final states = []; - final bloc = CounterBlocMixin(); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - }); - - group('redo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - final subscription = bloc.stream.listen(states.add); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when no undos have occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2]); - }); - - test('works when one undo has occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test('does nothing when undos have been exhausted', () async { - final states = []; - final bloc = CounterBlocMixin(); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test( - 'does nothing when undos has occurred ' - 'followed by a new state change', () async { - final states = []; - final bloc = CounterBlocMixin(); - final subscription = bloc.stream.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..add(Decrement()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 0]); - }); - }); - }); - - group('ReplayBloc (legacy)', () { - group('undo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when limit is 0', () async { - final states = []; - final bloc = CounterBloc(limit: 0); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1]); - }); - - test('loses history outside of limit', () async { - final states = []; - final bloc = CounterBloc(limit: 1); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - - test('reverts to initial state', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 0]); - }); - - test('reverts to previous state with multiple state changes ', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - }); - - group('redo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when no undos have occurred', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2]); - }); - - test('works when one undo has occurred', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test('does nothing when undos have been exhausted', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test( - 'does nothing when undos has occurred ' - 'followed by a new state change', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..add(Decrement()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 0]); - }); - }); - }); - - group('ReplayBlocMixin (legacy)', () { - group('undo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when limit is 0', () async { - final states = []; - final bloc = CounterBlocMixin(limit: 0); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1]); - }); - - test('loses history outside of limit', () async { - final states = []; - final bloc = CounterBlocMixin(limit: 1); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - - test('reverts to initial state', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 0]); - }); - - test('reverts to previous state with multiple state changes ', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - }); - - group('redo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when no undos have occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2]); - }); - - test('works when one undo has occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test('does nothing when undos have been exhausted', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test( - 'does nothing when undos has occurred ' - 'followed by a new state change', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..add(Decrement()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 0]); - }); - }); - }); -} diff --git a/packages/replay_bloc/test/replay_bloc_test.dart b/packages/replay_bloc/test/replay_bloc_test.dart index a44a1a7d84c..d56a5c71c05 100644 --- a/packages/replay_bloc/test/replay_bloc_test.dart +++ b/packages/replay_bloc/test/replay_bloc_test.dart @@ -564,318 +564,4 @@ void main() { }); }); }); - - group('ReplayBloc (legacy)', () { - group('undo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when limit is 0', () async { - final states = []; - final bloc = CounterBloc(limit: 0); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1]); - }); - - test('loses history outside of limit', () async { - final states = []; - final bloc = CounterBloc(limit: 1); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - - test('reverts to initial state', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 0]); - }); - - test('reverts to previous state with multiple state changes ', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - }); - - group('redo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when no undos have occurred', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2]); - }); - - test('works when one undo has occurred', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test('does nothing when undos have been exhausted', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test( - 'does nothing when undos has occurred ' - 'followed by a new state change', () async { - final states = []; - final bloc = CounterBloc(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..add(Decrement()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 0]); - }); - }); - }); - - group('ReplayBlocMixin (legacy)', () { - group('undo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when limit is 0', () async { - final states = []; - final bloc = CounterBlocMixin(limit: 0); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1]); - }); - - test('loses history outside of limit', () async { - final states = []; - final bloc = CounterBlocMixin(limit: 1); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - - test('reverts to initial state', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 0]); - }); - - test('reverts to previous state with multiple state changes ', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.undo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - }); - - group('redo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when no undos have occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2]); - }); - - test('works when one undo has occurred', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test('does nothing when undos have been exhausted', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..redo() - ..redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test( - 'does nothing when undos has occurred ' - 'followed by a new state change', () async { - final states = []; - final bloc = CounterBlocMixin(); - // ignore: deprecated_member_use - final subscription = bloc.listen(states.add); - bloc - ..add(Increment()) - ..add(Increment()); - await Future.delayed(Duration.zero); - bloc - ..undo() - ..add(Decrement()); - await Future.delayed(Duration.zero); - bloc.redo(); - await bloc.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 0]); - }); - }); - }); } diff --git a/packages/replay_bloc/test/replay_cubit_test.dart b/packages/replay_bloc/test/replay_cubit_test.dart index 1c29b26783c..3ffc290c22e 100644 --- a/packages/replay_bloc/test/replay_cubit_test.dart +++ b/packages/replay_bloc/test/replay_cubit_test.dart @@ -490,296 +490,4 @@ void main() { }); }); }); - - group('ReplayCubit (legacy)', () { - group('undo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final cubit = CounterCubit(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit.undo(); - await cubit.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when limit is 0', () async { - final states = []; - final cubit = CounterCubit(limit: 0); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..undo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1]); - }); - - test('loses history outside of limit', () async { - final states = []; - final cubit = CounterCubit(limit: 1); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..undo() - ..undo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - - test('reverts to initial state', () async { - final states = []; - final cubit = CounterCubit(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..undo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 0]); - }); - - test('reverts to previous state with multiple state changes ', () async { - final states = []; - final cubit = CounterCubit(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..undo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - }); - - group('redo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final cubit = CounterCubit(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit.redo(); - await cubit.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when no undos have occurred', () async { - final states = []; - final cubit = CounterCubit(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..redo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2]); - }); - - test('works when one undo has occurred', () async { - final states = []; - final cubit = CounterCubit(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..undo() - ..redo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test('does nothing when undos have been exhausted', () async { - final states = []; - final cubit = CounterCubit(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..undo() - ..redo() - ..redo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test( - 'does nothing when undos has occurred ' - 'followed by a new state change', () async { - final states = []; - final cubit = CounterCubit(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..undo() - ..decrement() - ..redo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 0]); - }); - }); - }); - - group('ReplayMixin (legacy)', () { - group('undo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final cubit = CounterCubitMixin(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit.undo(); - await cubit.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when limit is 0', () async { - final states = []; - final cubit = CounterCubitMixin(limit: 0); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..undo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1]); - }); - - test('loses history outside of limit', () async { - final states = []; - final cubit = CounterCubitMixin(limit: 1); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..undo() - ..undo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - - test('reverts to initial state', () async { - final states = []; - final cubit = CounterCubitMixin(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..undo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 0]); - }); - - test('reverts to previous state with multiple state changes ', () async { - final states = []; - final cubit = CounterCubitMixin(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..undo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1]); - }); - }); - - group('redo', () { - test('does nothing when no state changes have occurred', () async { - final states = []; - final cubit = CounterCubitMixin(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - await Future.delayed(Duration.zero, cubit.redo); - await cubit.close(); - await subscription.cancel(); - expect(states, isEmpty); - }); - - test('does nothing when no undos have occurred', () async { - final states = []; - final cubit = CounterCubitMixin(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..redo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2]); - }); - - test('works when one undo has occurred', () async { - final states = []; - final cubit = CounterCubitMixin(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..undo() - ..redo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test('does nothing when undos have been exhausted', () async { - final states = []; - final cubit = CounterCubitMixin(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..undo() - ..redo() - ..redo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 2]); - }); - - test( - 'does nothing when undos has occurred ' - 'followed by a new state change', () async { - final states = []; - final cubit = CounterCubitMixin(); - // ignore: deprecated_member_use - final subscription = cubit.listen(states.add); - cubit - ..increment() - ..increment() - ..undo() - ..decrement() - ..redo(); - await cubit.close(); - await subscription.cancel(); - expect(states, const [1, 2, 1, 0]); - }); - }); - }); }