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/examples/flutter_weather/lib/main.dart b/examples/flutter_weather/lib/main.dart index a294ea3f6c8..82d1515b06d 100644 --- a/examples/flutter_weather/lib/main.dart +++ b/examples/flutter_weather/lib/main.dart @@ -8,13 +8,14 @@ import 'package:weather_repository/weather_repository.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - HydratedBloc.storage = await HydratedStorage.build( + final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); - BlocOverrides.runZoned( + 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 7c19cdc2132..7222e159328 100644 --- a/examples/flutter_weather/test/app_test.dart +++ b/examples/flutter_weather/test/app_test.dart @@ -15,8 +15,6 @@ class MockThemeCubit extends MockCubit implements ThemeCubit {} class MockWeatherRepository extends Mock implements WeatherRepository {} void main() { - setUpAll(initHydratedBloc); - group('WeatherApp', () { late WeatherRepository weatherRepository; @@ -25,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); }); }); @@ -41,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/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 bd3f82e2f46..dfd7802d961 100644 --- a/examples/flutter_weather/test/weather/view/weather_page_test.dart +++ b/examples/flutter_weather/test/weather/view/weather_page_test.dart @@ -21,8 +21,6 @@ class MockWeatherCubit extends MockCubit implements WeatherCubit { } void main() { - setUpAll(initHydratedBloc); - group('WeatherPage', () { late WeatherRepository weatherRepository; @@ -31,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); }); }); @@ -102,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/hydrated_bloc/example/lib/main.dart b/packages/hydrated_bloc/example/lib/main.dart index 8b050285fd5..240ebf8ce4c 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 { @@ -84,7 +87,7 @@ class CounterView extends StatelessWidget { FloatingActionButton( child: const Icon(Icons.delete_forever), onPressed: () async { - await HydratedBloc.storage.clear(); + await HydratedBlocOverrides.current?.storage.clear(); context.read().add(Reset()); }, ), diff --git a/packages/hydrated_bloc/lib/src/hydrated_bloc.dart b/packages/hydrated_bloc/lib/src/hydrated_bloc.dart index 9651c7deeca..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,12 +236,11 @@ 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); @@ -310,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. @@ -355,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' + ');'; } } @@ -412,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/test/e2e_test.dart b/packages/hydrated_bloc/test/e2e_test.dart index 0bc549aa6f9..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,308 +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.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.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()), - ); + 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 374fb0e5ccc..af1a8e82ebe 100644 --- a/packages/hydrated_bloc/test/hydrated_bloc_test.dart +++ b/packages/hydrated_bloc/test/hydrated_bloc_test.dart @@ -126,328 +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)); - // ignore: invalid_use_of_protected_member - verify(() => bloc.onError(expectedError, any())).called(2); - }, (error, _) { - expect(error.toString(), 'Exception: oops'); - }); + 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.toString(), - '''Converting object to an encodable object failed: Object''', - ); - })); - }); - - 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 f57fb0497fb..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,154 +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.toString(), 'Exception: oops'); - }, - ); + 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 9374d72fc5a..c1bdb42b997 100644 --- a/packages/hydrated_bloc/test/hydrated_storage_test.dart +++ b/packages/hydrated_bloc/test/hydrated_storage_test.dart @@ -13,6 +13,15 @@ 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);