diff --git a/.github/workflows/bloc_presentation_test-test.yml b/.github/workflows/bloc_presentation_test-test.yml index a7a17ad..e4cdd5c 100644 --- a/.github/workflows/bloc_presentation_test-test.yml +++ b/.github/workflows/bloc_presentation_test-test.yml @@ -46,6 +46,9 @@ jobs: - name: Run analyzer run: flutter analyze --fatal-warnings --fatal-infos + - name: Run tests + run: flutter test + - name: Dry run pub publish uses: leancodepl/mobile-tools/.github/actions/pub-release@pub-release-v1 with: diff --git a/packages/bloc_presentation_test/CHANGELOG.md b/packages/bloc_presentation_test/CHANGELOG.md index ecf12c8..013de60 100644 --- a/packages/bloc_presentation_test/CHANGELOG.md +++ b/packages/bloc_presentation_test/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.0-pre.1 + +- Add `blocPresentationTest` function + # 0.3.0 - Bump dependency on `bloc_presentation` to `^0.4.0` (#25) @@ -14,4 +18,4 @@ # 0.1.0 -- Initial release. Includes `MockPresentationBloc` / `MockPresentationCubit` and `whenListenPresentation`. +- Initial release. Includes `MockPresentationBloc`/`MockPresentationCubit` and `whenListenPresentation`. diff --git a/packages/bloc_presentation_test/example/lib/main.dart b/packages/bloc_presentation_test/example/lib/main.dart index 2a25aa3..788506d 100644 --- a/packages/bloc_presentation_test/example/lib/main.dart +++ b/packages/bloc_presentation_test/example/lib/main.dart @@ -9,12 +9,40 @@ import 'package:test/test.dart'; class CounterCubit extends Cubit with BlocPresentationMixin { CounterCubit() : super(0); + + void count() { + final newNumber = state + 1; + + if (state < 10) { + emitPresentation(CounterPresentationEvent(newNumber)); + } + + emit(newNumber); + } } sealed class CounterCubitEvent {} final class CounterPresentationEvent implements CounterCubitEvent { - const CounterPresentationEvent(); + const CounterPresentationEvent(this.number); + + final int number; + + @override + int get hashCode => number; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + return other is CounterPresentationEvent && other.number == number; + } } class MockCounterPresentationCubit @@ -26,6 +54,7 @@ class MockCounterCubit extends MockCubit implements CounterCubit {} void main() { mainMockPresentationCubit(); mainWhenListenPresentation(); + mainBlocPresentationTest(); } void mainMockPresentationCubit() { @@ -42,13 +71,13 @@ void mainMockPresentationCubit() { test( 'presentation stream emits events properly', () async { - mockCubit.emitMockPresentation(const CounterPresentationEvent()); + mockCubit.emitMockPresentation(const CounterPresentationEvent(5)); await expectLater( mockCubit.presentation, emitsInOrder( [ - equals(const CounterPresentationEvent()), + equals(const CounterPresentationEvent(5)), ], ), ); @@ -64,7 +93,7 @@ void mainWhenListenPresentation() { mockCubit = MockCounterCubit(); controller = whenListenPresentation( mockCubit, - initialEvents: [const CounterPresentationEvent()], + initialEvents: [const CounterPresentationEvent(1)], ); }); @@ -75,17 +104,59 @@ void mainWhenListenPresentation() { test( 'presentation stream emits events properly', () async { - controller.add(const CounterPresentationEvent()); + controller.add(const CounterPresentationEvent(2)); await expectLater( mockCubit.presentation, emitsInOrder( [ - equals(const CounterPresentationEvent()), - equals(const CounterPresentationEvent()), + equals(const CounterPresentationEvent(1)), + equals(const CounterPresentationEvent(2)), ], ), ); }, ); } + +void mainBlocPresentationTest() { + CounterCubit buildCubit() => CounterCubit(); + + blocPresentationTest( + 'emits correct presentation event after calling count when state was ' + 'smaller than 10', + build: buildCubit, + act: (cubit) => cubit.count(), + expectPresentation: () => const [ + CounterPresentationEvent(1), + ], + ); + + blocPresentationTest( + 'does not emit presentation event if count has not ben called', + build: buildCubit, + expectPresentation: () => const [], + ); + + blocPresentationTest( + 'emits correct presentation event after calling count 2 times when state ' + 'was smaller than 10', + build: buildCubit, + act: (cubit) => cubit + ..count() + ..count(), + // skipPresentation can be used for skipping events during verification + skipPresentation: 1, + expectPresentation: () => const [ + CounterPresentationEvent(2), + ], + ); + + blocPresentationTest( + 'does not emit presentation event after calling count when state was 10', + build: buildCubit, + seed: () => 10, + act: (cubit) => cubit.count(), + expectPresentation: () => const [], + ); +} diff --git a/packages/bloc_presentation_test/lib/bloc_presentation_test.dart b/packages/bloc_presentation_test/lib/bloc_presentation_test.dart index 36d934a..6a477c6 100644 --- a/packages/bloc_presentation_test/lib/bloc_presentation_test.dart +++ b/packages/bloc_presentation_test/lib/bloc_presentation_test.dart @@ -1,5 +1,6 @@ /// A testing library for bloc_presentation. library; +export 'src/bloc_presentation_test.dart'; export 'src/mock_presentation_bloc.dart'; export 'src/when_listen_presentation.dart'; diff --git a/packages/bloc_presentation_test/lib/src/bloc_presentation_test.dart b/packages/bloc_presentation_test/lib/src/bloc_presentation_test.dart new file mode 100644 index 0000000..7e385ef --- /dev/null +++ b/packages/bloc_presentation_test/lib/src/bloc_presentation_test.dart @@ -0,0 +1,180 @@ +import 'dart:async'; + +import 'package:bloc_presentation/bloc_presentation.dart'; +import 'package:diff_match_patch/diff_match_patch.dart'; +import 'package:meta/meta.dart'; +import 'package:test/test.dart'; + +/// This function provides a possibility to test blocs/cubits with +/// [BlocPresentationMixin] mixed in. +/// +/// Events emitted via presentation stream can be verified by +/// [expectPresentation]. +/// +/// This function is not intended to verify states, but +/// only to verify presentation events. +@isTest +void blocPresentationTest, State, P>( + String description, { + required B Function() build, + FutureOr Function()? setUp, + State Function()? seed, + dynamic Function(B bloc)? act, + Duration? wait, + int skipPresentation = 0, + required dynamic Function() expectPresentation, + FutureOr Function()? tearDown, + dynamic tags, +}) { + test( + description, + () async { + await testBlocPresentation( + setUp: setUp, + build: build, + seed: seed, + act: act, + wait: wait, + skipPresentation: skipPresentation, + expectPresentation: expectPresentation, + tearDown: tearDown, + ); + }, + tags: tags, + ); +} + +/// Internal [blocPresentationTest] runner which is only visible for testing. +/// This should never be used directly -- please use [blocPresentationTest] +/// instead. +@visibleForTesting +Future + testBlocPresentation, State, P>({ + FutureOr Function()? setUp, + required B Function() build, + State Function()? seed, + dynamic Function(B bloc)? act, + Duration? wait, + int skipPresentation = 0, + required dynamic Function() expectPresentation, + FutureOr Function()? tearDown, + dynamic tags, +}) async { + var shallowEquality = false; + + try { + await _runZonedGuarded(() async { + await setUp?.call(); + + final events =

[]; + final bloc = build(); + + if (seed != null) { + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + bloc.emit(seed()); + } + + final subscription = + bloc.presentation.skip(skipPresentation).listen(events.add); + + await act?.call(bloc); + + if (wait != null) { + await Future.delayed(wait); + } + + await Future.delayed(Duration.zero); + await bloc.close(); + + final dynamic expected = expectPresentation(); + shallowEquality = '$events' == '$expected'; + + try { + expect(events, wrapMatcher(expected)); + } on TestFailure catch (e) { + if (shallowEquality || expected is! List

) { + rethrow; + } + + final diff = _diff(expected: expected, actual: events); + final message = '${e.message}\n$diff'; + + // ignore: only_throw_errors + throw TestFailure(message); + } + + await subscription.cancel(); + await tearDown?.call(); + }); + } catch (e) { + if (shallowEquality && e is TestFailure) { + // ignore: only_throw_errors + throw TestFailure( + ''' +${e.message} +WARNING: Please ensure presentation events extend Equatable, override == and hashCode, or implement Comparable. +Alternatively, consider using Matchers in the expectPresentation of the blocPresentationTest rather than concrete presentation events instances.\n''', + ); + } + + rethrow; + } +} + +// Based on bloc_test package +Future _runZonedGuarded(Future Function() body) { + final completer = Completer(); + + runZonedGuarded(() async { + await body(); + + if (!completer.isCompleted) { + completer.complete(); + } + }, (e, st) { + if (!completer.isCompleted) { + completer.completeError(e, st); + } + }); + + return completer.future; +} + +// Based on bloc_test package +String _diff({required dynamic expected, required dynamic actual}) { + final buffer = StringBuffer(); + final differences = diff(expected.toString(), actual.toString()); + + buffer + ..writeln('${'=' * 4} diff ${'=' * 40}') + ..writeln() + ..writeln(differences.toPrettyString()) + ..writeln() + ..writeln('${'=' * 4} end diff ${'=' * 36}'); + + return buffer.toString(); +} + +extension on List { + // Based on bloc_test package + String toPrettyString() { + String identical(String str) => '\u001b[90m$str\u001B[0m'; + String deletion(String str) => '\u001b[31m[-$str-]\u001B[0m'; + String insertion(String str) => '\u001b[32m{+$str+}\u001B[0m'; + + final buffer = StringBuffer(); + + for (final difference in this) { + switch (difference.operation) { + case DIFF_EQUAL: + buffer.write(identical(difference.text)); + case DIFF_DELETE: + buffer.write(deletion(difference.text)); + case DIFF_INSERT: + buffer.write(insertion(difference.text)); + } + } + + return buffer.toString(); + } +} diff --git a/packages/bloc_presentation_test/pubspec.yaml b/packages/bloc_presentation_test/pubspec.yaml index 2f23079..83d71f9 100644 --- a/packages/bloc_presentation_test/pubspec.yaml +++ b/packages/bloc_presentation_test/pubspec.yaml @@ -1,6 +1,6 @@ name: bloc_presentation_test description: A testing library for Blocs/Cubits which mixin BlocPresentationMixin. To be used with bloc_presentation package. -version: 0.3.0 +version: 1.0.0-pre.1 homepage: https://github.com/leancodepl/bloc_presentation/tree/master/packages/bloc_presentation_test environment: @@ -10,9 +10,13 @@ environment: dependencies: bloc: ^8.0.0 bloc_presentation: ^0.4.0 + diff_match_patch: ^0.4.1 flutter: sdk: flutter + meta: ^1.9.1 mocktail: ^1.0.0 + test: ^1.24.3 dev_dependencies: + equatable: ^2.0.5 leancode_lint: '>=5.0.0' diff --git a/packages/bloc_presentation_test/test/bloc_presentation_test_test.dart b/packages/bloc_presentation_test/test/bloc_presentation_test_test.dart new file mode 100644 index 0000000..b5343c2 --- /dev/null +++ b/packages/bloc_presentation_test/test/bloc_presentation_test_test.dart @@ -0,0 +1,452 @@ +import 'dart:async'; + +import 'package:bloc_presentation_test/src/bloc_presentation_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +import 'cubits/cubits.dart'; + +class _MockRepository extends Mock implements Repository {} + +void main() { + group('blocPresentationTest', () { + group('CounterCubit', () { + blocPresentationTest( + 'emits [] when nothing is called', + build: CounterCubit.new, + expectPresentation: () => const [], + ); + + blocPresentationTest( + 'emits [IncrementPresentationEvent(1)] when increment is called', + build: CounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + ], + ); + + blocPresentationTest( + 'emits [IncrementPresentationEvent(1)] when increment is called with ' + 'async act', + build: CounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + ], + ); + + blocPresentationTest( + 'emits [' + 'IncrementPresentationEvent(1) ' + 'IncrementPresentationEvent(2)' + '] when increment is called multiple times with async act', + build: CounterCubit.new, + act: (cubit) => cubit + ..increment() + ..increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + IncrementPresentationEvent(2), + ], + ); + + blocPresentationTest( + 'emits [IncrementPresentationEvent(3)] when increment is called and ' + 'seed is 2 with async act', + build: CounterCubit.new, + seed: () => 2, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(3), + ], + ); + + blocPresentationTest( + 'emits [IncrementPresentationEvent(2)] when increment is called 2 ' + 'times and skipPresentation is set to 1', + build: CounterCubit.new, + act: (cubit) => cubit + ..increment() + ..increment(), + skipPresentation: 1, + expectPresentation: () => const [ + IncrementPresentationEvent(2), + ], + ); + + test('fails immediately when exception occurs in act', () async { + final exception = Exception('oops'); + late Object actualError; + + final completer = Completer(); + + await runZonedGuarded(() async { + unawaited( + testBlocPresentation( + build: CounterCubit.new, + act: (_) => throw exception, + expectPresentation: () => const [ + IncrementPresentationEvent(1), + ], + ).then((_) => completer.complete()), + ); + + await completer.future; + }, (error, _) { + actualError = error; + + if (!completer.isCompleted) { + completer.complete(); + } + }); + + expect(actualError, exception); + }); + + test('calls tearDown once after the test', () async { + var tearDownCalls = 0; + + await testBlocPresentation( + build: AsyncCounterCubit.new, + expectPresentation: () => const [], + tearDown: () { + tearDownCalls++; + }, + ); + + expect(tearDownCalls, 1); + }); + + test( + 'fails immediately when expectation is list which has the same length ' + 'as actual list lists do not match ', + () async { + const expectedError = + 'Expected: [IncrementPresentationEvent:IncrementPresentationEvent(2)]\n' + ' Actual: [IncrementPresentationEvent:IncrementPresentationEvent(1)]\n' + ' Which: at location [0] is IncrementPresentationEvent: instead of IncrementPresentationEvent:\n' + '\n' + '==== diff ========================================\n' + '\n' + '\x1B[90m[IncrementPresentationEvent(\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( + testBlocPresentation( + build: CounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(2), + ], + ).then((_) => completer.complete()), + ); + + await completer.future; + }, (error, _) { + actualError = error; + + if (!completer.isCompleted) { + completer.complete(); + } + }); + + expect((actualError as TestFailure).message, expectedError); + }, + ); + + test( + 'fails immediately when expectation is list which is shorter than ' + 'actual list', + () async { + const expectedError = 'Expected: []\n' + ' Actual: [IncrementPresentationEvent:IncrementPresentationEvent(1)]\n' + ' Which: at location [0] is [IncrementPresentationEvent:IncrementPresentationEvent(1)] which longer than expected\n' + '\n' + '==== diff ========================================\n' + '\n' + '\x1B[90m[\x1B[0m\x1B[32m{+IncrementPresentationEvent(1)+}\x1B[0m\x1B[90m]\x1B[0m\n' + '\n' + '==== end diff ====================================\n'; + late Object actualError; + final completer = Completer(); + + await runZonedGuarded(() async { + unawaited( + testBlocPresentation( + build: CounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [], + ).then((_) => completer.complete()), + ); + + await completer.future; + }, (error, _) { + actualError = error; + + if (!completer.isCompleted) { + completer.complete(); + } + }); + + expect((actualError as TestFailure).message, expectedError); + }, + ); + + test( + 'fails immediately when expectation is list which is longer than ' + 'actual list', + () async { + const expectedError = 'Expected: [\n' + ' IncrementPresentationEvent:IncrementPresentationEvent(1),\n' + ' IncrementPresentationEvent:IncrementPresentationEvent(2)\n' + ' ]\n' + ' Actual: [IncrementPresentationEvent:IncrementPresentationEvent(1)]\n' + ' Which: at location [1] is [IncrementPresentationEvent:IncrementPresentationEvent(1)] which shorter than expected\n' + '\n' + '==== diff ========================================\n' + '\n' + '\x1B[90m[IncrementPresentationEvent(1)\x1B[0m\x1B[31m[-, IncrementPresentationEvent(2)-]\x1B[0m\x1B[90m]\x1B[0m\n' + '\n' + '==== end diff ====================================\n'; + late Object actualError; + final completer = Completer(); + + await runZonedGuarded(() async { + unawaited( + testBlocPresentation( + build: CounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + IncrementPresentationEvent(2), + ], + ).then((_) => completer.complete()), + ); + + await completer.future; + }, (error, _) { + actualError = error; + + if (!completer.isCompleted) { + completer.complete(); + } + }); + + expect((actualError as TestFailure).message, expectedError); + }, + ); + }); + + group('AsyncCounterCubit', () { + blocPresentationTest( + 'emits [] when nothing is called', + build: AsyncCounterCubit.new, + expectPresentation: () => const [], + ); + + blocPresentationTest( + 'emits [IncrementPresentationEvent(1)] when increment is called', + build: AsyncCounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + ], + ); + + blocPresentationTest( + 'emits [' + 'IncrementPresentationEvent(1) ' + 'IncrementPresentationEvent(2)' + '] when increment is called multiple times with async act', + build: AsyncCounterCubit.new, + act: (cubit) async { + await cubit.increment(); + await cubit.increment(); + }, + expectPresentation: () => const [ + IncrementPresentationEvent(1), + IncrementPresentationEvent(2), + ], + ); + }); + + group('DelayedCounterCubit', () { + blocPresentationTest( + 'emits [] when nothing is called', + build: DelayedCounterCubit.new, + expectPresentation: () => [], + ); + + blocPresentationTest( + 'emits [] when increment is called without wait', + build: DelayedCounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [], + ); + + blocPresentationTest( + 'emits [IncrementPresentationEvent(1)] when increment is called with ' + 'wait', + build: DelayedCounterCubit.new, + act: (cubit) => cubit.increment(), + wait: const Duration(milliseconds: 300), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + ], + ); + }); + + group('MultiCounterCubit', () { + blocPresentationTest( + 'emits [] when nothing is called', + build: MultiCounterCubit.new, + expectPresentation: () => [], + ); + + blocPresentationTest( + 'emits [' + 'IncrementPresentationEvent(1) ' + 'IncrementPresentationEvent(2)' + '] when increment is called', + build: MultiCounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + IncrementPresentationEvent(2), + ], + ); + + blocPresentationTest( + 'emits [' + 'IncrementPresentationEvent(1) ' + 'IncrementPresentationEvent(2) ' + 'IncrementPresentationEvent(3) ' + 'IncrementPresentationEvent(4)' + '] when increment is called ' + 'multiple times with async act', + build: MultiCounterCubit.new, + act: (cubit) => cubit + ..increment() + ..increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + IncrementPresentationEvent(2), + IncrementPresentationEvent(3), + IncrementPresentationEvent(4), + ], + ); + }); + + group('SideEffectCounterCubit', () { + late _MockRepository repository; + + setUp(() { + repository = _MockRepository(); + }); + + blocPresentationTest( + 'emits [] when increment is called and repository.increment returned ' + 'false', + setUp: () { + when(repository.increment).thenReturn(false); + }, + build: () => SideEffectCounterCubit(repository), + act: (cubit) => cubit.increment(), + expectPresentation: () => const [], + ); + + blocPresentationTest( + 'emits [IncrementPresentationEvent(1)] when increment is called and ' + 'repository.increment returned false', + setUp: () { + when(repository.increment).thenReturn(true); + }, + build: () => SideEffectCounterCubit(repository), + act: (cubit) => cubit.increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + ], + ); + }); + + group( + 'ExceptionCounterCubit', + () { + test('future still completes when uncaught exception occurs', () async { + await expectLater( + () => testBlocPresentation( + build: ExceptionCounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + ], + ), + throwsA(isA()), + ); + }); + }, + ); + + group( + 'ErrorCounterCubit', + () { + test('future still completes when uncaught error occurs', () async { + await expectLater( + () => testBlocPresentation( + build: ErrorCounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => const [ + IncrementPresentationEvent(1), + ], + ), + throwsA(isA()), + ); + }); + }, + ); + + group('NonEquatableCounterCubit', () { + test('adds additional warning to thrown exception message', () async { + const warning = '\n' + 'WARNING: Please ensure presentation events extend Equatable, override == and hashCode, or implement Comparable.\n' + 'Alternatively, consider using Matchers in the expectPresentation of the blocPresentationTest rather than concrete presentation events instances.\n'; + late Object actualError; + final completer = Completer(); + + await runZonedGuarded(() async { + unawaited( + testBlocPresentation( + build: NonEquatableCounterCubit.new, + act: (cubit) => cubit.increment(), + expectPresentation: () => + const [ + NonEquatableIncrementPresentationEvent(1), + ], + ).then((_) => completer.complete()), + ); + + await completer.future; + }, (error, _) { + actualError = error; + + if (!completer.isCompleted) { + completer.complete(); + } + }); + + expect((actualError as TestFailure).message?.contains(warning), true); + }); + }); + }); +} diff --git a/packages/bloc_presentation_test/test/cubits/async_counter_cubit.dart b/packages/bloc_presentation_test/test/cubits/async_counter_cubit.dart new file mode 100644 index 0000000..529b213 --- /dev/null +++ b/packages/bloc_presentation_test/test/cubits/async_counter_cubit.dart @@ -0,0 +1,19 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc_presentation/bloc_presentation.dart'; + +import 'events.dart'; + +class AsyncCounterCubit extends Cubit + with BlocPresentationMixin { + AsyncCounterCubit() : super(0); + + Future increment() async { + final newNumber = state + 1; + + await Future.delayed(const Duration(microseconds: 1)); + + emitPresentation(IncrementPresentationEvent(newNumber)); + + emit(newNumber); + } +} diff --git a/packages/bloc_presentation_test/test/cubits/counter_cubit.dart b/packages/bloc_presentation_test/test/cubits/counter_cubit.dart new file mode 100644 index 0000000..f8a73a9 --- /dev/null +++ b/packages/bloc_presentation_test/test/cubits/counter_cubit.dart @@ -0,0 +1,17 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc_presentation/bloc_presentation.dart'; + +import 'events.dart'; + +class CounterCubit extends Cubit + with BlocPresentationMixin { + CounterCubit() : super(0); + + void increment() { + final newNumber = state + 1; + + emitPresentation(IncrementPresentationEvent(newNumber)); + + emit(newNumber); + } +} diff --git a/packages/bloc_presentation_test/test/cubits/cubits.dart b/packages/bloc_presentation_test/test/cubits/cubits.dart new file mode 100644 index 0000000..388d067 --- /dev/null +++ b/packages/bloc_presentation_test/test/cubits/cubits.dart @@ -0,0 +1,9 @@ +export 'async_counter_cubit.dart'; +export 'counter_cubit.dart'; +export 'delayed_counter_cubit.dart'; +export 'error_counter_cubit.dart'; +export 'events.dart'; +export 'exception_counter_cubit.dart'; +export 'multi_counter_cubit.dart'; +export 'non_equatable_counter_cubit.dart'; +export 'side_effect_counter_cubit.dart'; diff --git a/packages/bloc_presentation_test/test/cubits/delayed_counter_cubit.dart b/packages/bloc_presentation_test/test/cubits/delayed_counter_cubit.dart new file mode 100644 index 0000000..7ba2106 --- /dev/null +++ b/packages/bloc_presentation_test/test/cubits/delayed_counter_cubit.dart @@ -0,0 +1,24 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc_presentation/bloc_presentation.dart'; + +import 'events.dart'; + +class DelayedCounterCubit extends Cubit + with BlocPresentationMixin { + DelayedCounterCubit() : super(0); + + void increment() { + Future.delayed( + const Duration(milliseconds: 300), + () { + if (!isClosed) { + final newNumber = state + 1; + + emitPresentation(IncrementPresentationEvent(newNumber)); + + emit(newNumber); + } + }, + ); + } +} diff --git a/packages/bloc_presentation_test/test/cubits/error_counter_cubit.dart b/packages/bloc_presentation_test/test/cubits/error_counter_cubit.dart new file mode 100644 index 0000000..c71f176 --- /dev/null +++ b/packages/bloc_presentation_test/test/cubits/error_counter_cubit.dart @@ -0,0 +1,15 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc_presentation/bloc_presentation.dart'; + +import 'events.dart'; + +class CounterError extends Error {} + +class ErrorCounterCubit extends Cubit + with BlocPresentationMixin { + ErrorCounterCubit() : super(0); + + void increment() { + throw CounterError(); + } +} diff --git a/packages/bloc_presentation_test/test/cubits/events.dart b/packages/bloc_presentation_test/test/cubits/events.dart new file mode 100644 index 0000000..72466e1 --- /dev/null +++ b/packages/bloc_presentation_test/test/cubits/events.dart @@ -0,0 +1,25 @@ +import 'package:equatable/equatable.dart'; + +sealed class CounterPresentationEvent extends Equatable { + const CounterPresentationEvent(this.number); + + final int number; +} + +final class IncrementPresentationEvent extends CounterPresentationEvent { + const IncrementPresentationEvent(super.number); + + @override + List get props => [number]; +} + +sealed class NonEquatableCounterPresentationEvent { + const NonEquatableCounterPresentationEvent(this.number); + + final int number; +} + +final class NonEquatableIncrementPresentationEvent + extends NonEquatableCounterPresentationEvent { + const NonEquatableIncrementPresentationEvent(super.number); +} diff --git a/packages/bloc_presentation_test/test/cubits/exception_counter_cubit.dart b/packages/bloc_presentation_test/test/cubits/exception_counter_cubit.dart new file mode 100644 index 0000000..e2761f9 --- /dev/null +++ b/packages/bloc_presentation_test/test/cubits/exception_counter_cubit.dart @@ -0,0 +1,15 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc_presentation/bloc_presentation.dart'; + +import 'events.dart'; + +class CounterException implements Exception {} + +class ExceptionCounterCubit extends Cubit + with BlocPresentationMixin { + ExceptionCounterCubit() : super(0); + + void increment() { + throw CounterException(); + } +} diff --git a/packages/bloc_presentation_test/test/cubits/multi_counter_cubit.dart b/packages/bloc_presentation_test/test/cubits/multi_counter_cubit.dart new file mode 100644 index 0000000..d979263 --- /dev/null +++ b/packages/bloc_presentation_test/test/cubits/multi_counter_cubit.dart @@ -0,0 +1,21 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc_presentation/bloc_presentation.dart'; + +import 'events.dart'; + +class MultiCounterCubit extends Cubit + with BlocPresentationMixin { + MultiCounterCubit() : super(0); + + void increment() { + var newNumber = state + 1; + + emitPresentation(IncrementPresentationEvent(newNumber)); + + newNumber = ++newNumber; + + emitPresentation(IncrementPresentationEvent(newNumber)); + + emit(newNumber); + } +} diff --git a/packages/bloc_presentation_test/test/cubits/non_equatable_counter_cubit.dart b/packages/bloc_presentation_test/test/cubits/non_equatable_counter_cubit.dart new file mode 100644 index 0000000..423a0a3 --- /dev/null +++ b/packages/bloc_presentation_test/test/cubits/non_equatable_counter_cubit.dart @@ -0,0 +1,17 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc_presentation/bloc_presentation.dart'; + +import 'events.dart'; + +class NonEquatableCounterCubit extends Cubit + with BlocPresentationMixin { + NonEquatableCounterCubit() : super(0); + + void increment() { + final newNumber = state + 1; + + emitPresentation(NonEquatableIncrementPresentationEvent(newNumber)); + + emit(newNumber); + } +} diff --git a/packages/bloc_presentation_test/test/cubits/side_effect_counter_cubit.dart b/packages/bloc_presentation_test/test/cubits/side_effect_counter_cubit.dart new file mode 100644 index 0000000..b1ffc87 --- /dev/null +++ b/packages/bloc_presentation_test/test/cubits/side_effect_counter_cubit.dart @@ -0,0 +1,27 @@ +import 'package:bloc/bloc.dart'; +import 'package:bloc_presentation/bloc_presentation.dart'; + +import 'events.dart'; + +class Repository { + bool increment() => true; +} + +class SideEffectCounterCubit extends Cubit + with BlocPresentationMixin { + SideEffectCounterCubit(this.repository) : super(0); + + final Repository repository; + + void increment() { + final incremented = repository.increment(); + + if (incremented) { + final newNumber = state + 1; + + emitPresentation(IncrementPresentationEvent(newNumber)); + + emit(newNumber); + } + } +}