diff --git a/.github/workflows/app_ui.yaml b/.github/workflows/app_ui.yaml new file mode 100644 index 0000000..41acf3c --- /dev/null +++ b/.github/workflows/app_ui.yaml @@ -0,0 +1,20 @@ +name: app_ui + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + paths: + - "packages/app_ui/**" + - ".github/workflows/app_ui.yaml" + branches: + - main + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + flutter_version: 3.16.0 + working_directory: packages/app_ui diff --git a/assets/animations/dash_animation.png b/assets/animations/dash_animation.png new file mode 100644 index 0000000..c27b558 Binary files /dev/null and b/assets/animations/dash_animation.png differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 558f731..8409138 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -21,13 +21,13 @@ class AppBlocObserver extends BlocObserver { } Future bootstrap(FutureOr Function() builder) async { + WidgetsFlutterBinding.ensureInitialized(); + FlutterError.onError = (details) { log(details.exceptionAsString(), stackTrace: details.stack); }; Bloc.observer = const AppBlocObserver(); - // Add cross-flavor configuration here - runApp(await builder()); } diff --git a/lib/counter/counter.dart b/lib/counter/counter.dart index cc3f0c5..c1f6927 100644 --- a/lib/counter/counter.dart +++ b/lib/counter/counter.dart @@ -1,2 +1 @@ -export 'cubit/counter_cubit.dart'; export 'view/counter_page.dart'; diff --git a/lib/counter/cubit/counter_cubit.dart b/lib/counter/cubit/counter_cubit.dart deleted file mode 100644 index 70bd952..0000000 --- a/lib/counter/cubit/counter_cubit.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:bloc/bloc.dart'; - -class CounterCubit extends Cubit { - CounterCubit() : super(0); - - void increment() => emit(state + 1); - void decrement() => emit(state - 1); -} diff --git a/lib/counter/view/counter_page.dart b/lib/counter/view/counter_page.dart index 422e6ab..3a131ad 100644 --- a/lib/counter/view/counter_page.dart +++ b/lib/counter/view/counter_page.dart @@ -1,17 +1,13 @@ -import 'package:dash_ai_search/counter/counter.dart'; +import 'package:app_ui/app_ui.dart'; import 'package:dash_ai_search/l10n/l10n.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class CounterPage extends StatelessWidget { const CounterPage({super.key}); @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => CounterCubit(), - child: const CounterView(), - ); + return const CounterView(); } } @@ -21,35 +17,37 @@ class CounterView extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + final screenSize = MediaQuery.sizeOf(context); return Scaffold( appBar: AppBar(title: Text(l10n.counterAppBarTitle)), - body: const Center(child: CounterText()), - floatingActionButton: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - FloatingActionButton( - onPressed: () => context.read().increment(), - child: const Icon(Icons.add), - ), - const SizedBox(height: 8), - FloatingActionButton( - onPressed: () => context.read().decrement(), - child: const Icon(Icons.remove), - ), - ], + body: Center( + child: Container( + alignment: Alignment.center, + height: screenSize.height / 2, + width: screenSize.height / 2, + child: const DashAnimation(), + ), ), ); } } -class CounterText extends StatelessWidget { - const CounterText({super.key}); +class DashAnimation extends StatelessWidget { + @visibleForTesting + const DashAnimation({super.key}); + + static const dashSize = Size(800, 800); @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final count = context.select((CounterCubit cubit) => cubit.state); - return Text('$count', style: theme.textTheme.displayLarge); + return const AnimatedSprite( + showLoadingIndicator: false, + sprites: Sprites( + asset: 'dash_animation.png', + size: dashSize, + frames: 34, + stepTime: 0.07, + ), + ); } } diff --git a/packages/app_ui/.gitignore b/packages/app_ui/.gitignore new file mode 100644 index 0000000..06ef8e6 --- /dev/null +++ b/packages/app_ui/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/* + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Test related +coverage \ No newline at end of file diff --git a/packages/app_ui/README.md b/packages/app_ui/README.md new file mode 100644 index 0000000..013f886 --- /dev/null +++ b/packages/app_ui/README.md @@ -0,0 +1,12 @@ +# App UI + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) +[![License: MIT][license_badge]][license_link] + +UI Toolkit + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/packages/app_ui/analysis_options.yaml b/packages/app_ui/analysis_options.yaml new file mode 100644 index 0000000..799268d --- /dev/null +++ b/packages/app_ui/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml diff --git a/packages/app_ui/coverage_badge.svg b/packages/app_ui/coverage_badge.svg new file mode 100644 index 0000000..499e98c --- /dev/null +++ b/packages/app_ui/coverage_badge.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + diff --git a/packages/app_ui/lib/app_ui.dart b/packages/app_ui/lib/app_ui.dart new file mode 100644 index 0000000..9ef06cd --- /dev/null +++ b/packages/app_ui/lib/app_ui.dart @@ -0,0 +1 @@ +export 'src/widgets/widgets.dart'; diff --git a/packages/app_ui/lib/src/widgets/animated_sprite.dart b/packages/app_ui/lib/src/widgets/animated_sprite.dart new file mode 100644 index 0000000..0319616 --- /dev/null +++ b/packages/app_ui/lib/src/widgets/animated_sprite.dart @@ -0,0 +1,159 @@ +import 'dart:async'; +import 'dart:ui' as ui show Image; + +import 'package:app_ui/app_ui.dart'; +import 'package:flame/components.dart' hide Timer; +import 'package:flame/flame.dart'; +import 'package:flame/sprite.dart'; +import 'package:flame/widgets.dart'; +import 'package:flutter/material.dart'; + +/// {@template sprites} +/// Object which contains meta data for a collection of sprites. +/// {@endtemplate} +class Sprites { + /// {@macro sprites} + const Sprites({ + required this.asset, + required this.size, + required this.frames, + this.stepTime = 0.1, + }); + + /// The sprite sheet asset name. + /// This should be the name of the file within + /// the `assets/images` directory. + final String asset; + + /// The size an individual sprite within the sprite sheet + final Size size; + + /// The number of frames within the sprite sheet. + final int frames; + + /// Number of seconds per frame. Defaults to 0.1. + final double stepTime; +} + +/// The animation mode which determines when the animation plays. +enum AnimationMode { + /// Animations plays on a loop + loop, + + /// Animations plays immediately once + oneTime +} + +/// {@template animated_sprite} +/// A widget which renders an animated sprite +/// given a collection of sprites. +/// {@endtemplate} +class AnimatedSprite extends StatefulWidget { + /// {@macro animated_sprite} + const AnimatedSprite({ + required this.sprites, + super.key, + this.mode = AnimationMode.loop, + this.showLoadingIndicator = true, + this.loadingIndicatorColor = Colors.white, + }); + + /// The collection of sprites which will be animated. + final Sprites sprites; + + /// The mode of animation (`trigger`, `loop` or `oneTime`). + final AnimationMode mode; + + /// Where should display a loading indicator while loading the sprite + final bool showLoadingIndicator; + + /// Color for loading indicator + final Color loadingIndicatorColor; + + @override + State createState() => _AnimatedSpriteState(); +} + +enum _AnimatedSpriteStatus { loading, loaded, failure } + +extension on _AnimatedSpriteStatus { + /// Returns true for `_AnimatedSpriteStatus.loaded`. + bool get isLoaded => this == _AnimatedSpriteStatus.loaded; +} + +class _AnimatedSpriteState extends State { + late SpriteAnimation _animation; + Timer? _timer; + var _status = _AnimatedSpriteStatus.loading; + var _isPlaying = false; + late SpriteAnimationTicker _spriteAnimationTicker; + + @override + void initState() { + super.initState(); + _loadAnimation(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + Future _loadAnimation() async { + late ui.Image image; + try { + final images = Flame.images..prefix = 'assets/animations/'; + image = await images.load(widget.sprites.asset); + } catch (_) { + setState(() => _status = _AnimatedSpriteStatus.failure); + return; + } + + _animation = SpriteSheet( + image: image, + srcSize: Vector2(widget.sprites.size.width, widget.sprites.size.height), + ).createAnimation( + row: 0, + stepTime: widget.sprites.stepTime, + to: widget.sprites.frames, + loop: widget.mode == AnimationMode.loop, + ); + _spriteAnimationTicker = _animation.createTicker(); + + setState(() { + _status = _AnimatedSpriteStatus.loaded; + if (widget.mode == AnimationMode.loop || + widget.mode == AnimationMode.oneTime) { + _isPlaying = true; + } + }); + } + + @override + Widget build(BuildContext context) { + return AppAnimatedCrossFade( + firstChild: widget.showLoadingIndicator + ? SizedBox.fromSize( + size: const Size(20, 20), + child: AppCircularProgressIndicator( + strokeWidth: 2, + color: widget.loadingIndicatorColor, + ), + ) + : const SizedBox(), + secondChild: SizedBox.expand( + child: _status.isLoaded + ? SpriteAnimationWidget( + animation: _animation, + playing: _isPlaying, + animationTicker: _spriteAnimationTicker, + ) + : const SizedBox(), + ), + crossFadeState: _status.isLoaded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + ); + } +} diff --git a/packages/app_ui/lib/src/widgets/app_animated_cross_fade.dart b/packages/app_ui/lib/src/widgets/app_animated_cross_fade.dart new file mode 100644 index 0000000..36c5803 --- /dev/null +++ b/packages/app_ui/lib/src/widgets/app_animated_cross_fade.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +/// {@template app_animated_cross_fade} +/// Abstraction of AnimatedCrossFade to override the layout centering +/// the widgets inside +/// {@endtemplate} +class AppAnimatedCrossFade extends StatelessWidget { + /// {@macro app_animated_cross_fade} + + const AppAnimatedCrossFade({ + required this.firstChild, + required this.secondChild, + required this.crossFadeState, + super.key, + }); + + /// First [Widget] to display + final Widget firstChild; + + /// Second [Widget] to display + final Widget secondChild; + + /// Specifies when to display [firstChild] or [secondChild] + final CrossFadeState crossFadeState; + + @override + Widget build(BuildContext context) { + return AnimatedCrossFade( + firstChild: firstChild, + secondChild: secondChild, + crossFadeState: crossFadeState, + duration: const Duration(seconds: 1), + layoutBuilder: ( + Widget topChild, + Key topChildKey, + Widget bottomChild, + Key bottomChildKey, + ) { + return Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + Align( + key: bottomChildKey, + child: bottomChild, + ), + Align( + key: topChildKey, + child: topChild, + ), + ], + ); + }, + ); + } +} diff --git a/packages/app_ui/lib/src/widgets/app_circular_progress_indicator.dart b/packages/app_ui/lib/src/widgets/app_circular_progress_indicator.dart new file mode 100644 index 0000000..d0bd89c --- /dev/null +++ b/packages/app_ui/lib/src/widgets/app_circular_progress_indicator.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +/// {@template app_circular_progress_indicator} +/// Circular progress indicator +/// {@endtemplate} +class AppCircularProgressIndicator extends StatelessWidget { + /// {@macro app_circular_progress_indicator} + const AppCircularProgressIndicator({ + super.key, + this.color = Colors.blue, + this.backgroundColor = Colors.white, + this.strokeWidth = 4.0, + }); + + /// [Color] of the progress indicator + final Color color; + + /// [Color] for the background + final Color? backgroundColor; + + /// Optional stroke width of the progress indicator + final double strokeWidth; + + @override + Widget build(BuildContext context) { + return CircularProgressIndicator( + color: color, + backgroundColor: backgroundColor, + strokeWidth: strokeWidth, + ); + } +} diff --git a/packages/app_ui/lib/src/widgets/widgets.dart b/packages/app_ui/lib/src/widgets/widgets.dart new file mode 100644 index 0000000..08e3705 --- /dev/null +++ b/packages/app_ui/lib/src/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'animated_sprite.dart'; +export 'app_animated_cross_fade.dart'; +export 'app_circular_progress_indicator.dart'; diff --git a/packages/app_ui/pubspec.yaml b/packages/app_ui/pubspec.yaml new file mode 100644 index 0000000..e7109d2 --- /dev/null +++ b/packages/app_ui/pubspec.yaml @@ -0,0 +1,19 @@ +name: app_ui +description: A Very Good Project created by Very Good CLI. +version: 0.1.0+1 +publish_to: none + +environment: + sdk: ">=3.1.0 <4.0.0" + flutter: 3.16.0 + +dependencies: + flame: ^1.10.1 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^1.0.0 + very_good_analysis: ^5.1.0 diff --git a/packages/app_ui/test/src/widgets/animated_sprite_test.dart b/packages/app_ui/test/src/widgets/animated_sprite_test.dart new file mode 100644 index 0000000..6df14a0 --- /dev/null +++ b/packages/app_ui/test/src/widgets/animated_sprite_test.dart @@ -0,0 +1,101 @@ +// ignore_for_file: prefer_const_constructors +import 'dart:ui' as ui show Image; + +import 'package:app_ui/app_ui.dart'; +import 'package:flame/cache.dart'; +import 'package:flame/flame.dart'; +import 'package:flame/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class __MockImages extends Mock implements Images {} + +void main() { + group('AnimatedSprite', () { + late Images images; + late ui.Image image; + + setUp(() async { + images = __MockImages(); + Flame.images = images; + image = await createTestImage(height: 10, width: 10); + }); + + setUpAll(TestWidgetsFlutterBinding.ensureInitialized); + + testWidgets('renders AppCircularProgressIndicator when loading asset', + (tester) async { + await tester.pumpWidget( + AnimatedSprite( + sprites: Sprites(asset: 'test.png', size: Size(1, 1), frames: 1), + ), + ); + expect(find.byType(AppCircularProgressIndicator), findsOneWidget); + }); + + testWidgets( + 'does not render AppCircularProgressIndicator' + ' when loading asset and showLoadingIndicator is false', + (tester) async { + await tester.pumpWidget( + AnimatedSprite( + sprites: Sprites(asset: 'test.png', size: Size(1, 1), frames: 1), + showLoadingIndicator: false, + ), + ); + expect(find.byType(AppCircularProgressIndicator), findsNothing); + }); + + testWidgets('renders SpriteAnimationWidget when asset is loaded (loop)', + (tester) async { + await tester.runAsync(() async { + final images = __MockImages(); + when(() => images.load(any())).thenAnswer((_) async => image); + Flame.images = images; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AnimatedSprite( + sprites: + Sprites(asset: 'test.png', size: Size(1, 1), frames: 1), + ), + ), + ), + ); + await tester.pump(); + final spriteAnimationFinder = find.byType(SpriteAnimationWidget); + final widget = tester.widget( + spriteAnimationFinder, + ); + expect(widget.playing, isTrue); + }); + }); + + testWidgets('renders SpriteAnimationWidget when asset is loaded (oneTime)', + (tester) async { + await tester.runAsync(() async { + final images = __MockImages(); + when(() => images.load(any())).thenAnswer((_) async => image); + Flame.images = images; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AnimatedSprite( + sprites: + Sprites(asset: 'test.png', size: Size(1, 1), frames: 1), + mode: AnimationMode.oneTime, + ), + ), + ), + ); + await tester.pump(); + final spriteAnimationFinder = find.byType(SpriteAnimationWidget); + final widget = tester.widget( + spriteAnimationFinder, + ); + expect(widget.playing, isTrue); + }); + }); + }); +} diff --git a/packages/app_ui/test/src/widgets/app_animated_cross_fade_test.dart b/packages/app_ui/test/src/widgets/app_animated_cross_fade_test.dart new file mode 100644 index 0000000..b6ec79e --- /dev/null +++ b/packages/app_ui/test/src/widgets/app_animated_cross_fade_test.dart @@ -0,0 +1,19 @@ +import 'package:app_ui/app_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AppAnimatedCrossFade', () { + testWidgets('renders both children', (tester) async { + await tester.pumpWidget( + const AppAnimatedCrossFade( + firstChild: SizedBox(key: Key('first')), + secondChild: SizedBox(key: Key('second')), + crossFadeState: CrossFadeState.showFirst, + ), + ); + expect(find.byKey(const Key('first')), findsOneWidget); + expect(find.byKey(const Key('second')), findsOneWidget); + }); + }); +} diff --git a/packages/app_ui/test/src/widgets/app_circular_progress_indicator_test.dart b/packages/app_ui/test/src/widgets/app_circular_progress_indicator_test.dart new file mode 100644 index 0000000..827786d --- /dev/null +++ b/packages/app_ui/test/src/widgets/app_circular_progress_indicator_test.dart @@ -0,0 +1,37 @@ +import 'package:app_ui/app_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AppCircularProgressIndicator', () { + testWidgets('renders', (tester) async { + await tester.pumpWidget(const AppCircularProgressIndicator()); + expect(find.byType(AppCircularProgressIndicator), findsOneWidget); + }); + + testWidgets('renders with default colors', (tester) async { + await tester.pumpWidget(const AppCircularProgressIndicator()); + final widget = tester.widget( + find.byType(AppCircularProgressIndicator), + ); + expect(widget.color, Colors.blue); + expect(widget.backgroundColor, Colors.white); + }); + + testWidgets('renders with provided colors', (tester) async { + const color = Colors.black; + const backgroundColor = Colors.blue; + await tester.pumpWidget( + const AppCircularProgressIndicator( + color: color, + backgroundColor: backgroundColor, + ), + ); + final widget = tester.widget( + find.byType(AppCircularProgressIndicator), + ); + expect(widget.color, color); + expect(widget.backgroundColor, backgroundColor); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index edc26bc..76bb4c3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -24,6 +24,13 @@ packages: relative: true source: path version: "0.1.0+1" + app_ui: + dependency: "direct main" + description: + path: "packages/app_ui" + relative: true + source: path + version: "0.1.0+1" args: dependency: transitive description: @@ -136,6 +143,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + flame: + dependency: "direct main" + description: + name: flame + sha256: b6bb76224fc29fd5eea25d66cda6e322e3678bdedc1f65956c6151326a6a798b + url: "https://pub.dev" + source: hosted + version: "1.10.1" flutter: dependency: "direct main" description: flutter @@ -287,6 +302,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + ordered_set: + dependency: transitive + description: + name: ordered_set + sha256: "3858c7d84619edfab87c3e367584648020903187edb70b52697646f4b2a93022" + url: "https://pub.dev" + source: hosted + version: "5.0.2" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 981ffcd..3e9da00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,10 @@ environment: dependencies: api_client: path: packages/api_client + app_ui: + path: packages/app_ui bloc: ^8.1.2 + flame: ^1.10.1 flutter: sdk: flutter flutter_bloc: ^8.1.3 @@ -28,3 +31,5 @@ dev_dependencies: flutter: uses-material-design: true generate: true + assets: + - assets/animations/ diff --git a/test/counter/cubit/counter_cubit_test.dart b/test/counter/cubit/counter_cubit_test.dart deleted file mode 100644 index 2f649c1..0000000 --- a/test/counter/cubit/counter_cubit_test.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:dash_ai_search/counter/counter.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('CounterCubit', () { - test('initial state is 0', () { - expect(CounterCubit().state, equals(0)); - }); - - blocTest( - 'emits [1] when increment is called', - build: CounterCubit.new, - act: (cubit) => cubit.increment(), - expect: () => [equals(1)], - ); - - blocTest( - 'emits [-1] when decrement is called', - build: CounterCubit.new, - act: (cubit) => cubit.decrement(), - expect: () => [equals(-1)], - ); - }); -} diff --git a/test/counter/view/counter_page_test.dart b/test/counter/view/counter_page_test.dart index 79ac31d..f2b98a4 100644 --- a/test/counter/view/counter_page_test.dart +++ b/test/counter/view/counter_page_test.dart @@ -1,14 +1,8 @@ -import 'package:bloc_test/bloc_test.dart'; import 'package:dash_ai_search/counter/counter.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; import '../../helpers/helpers.dart'; -class MockCounterCubit extends MockCubit implements CounterCubit {} - void main() { group('CounterPage', () { testWidgets('renders CounterView', (tester) async { @@ -16,52 +10,4 @@ void main() { expect(find.byType(CounterView), findsOneWidget); }); }); - - group('CounterView', () { - late CounterCubit counterCubit; - - setUp(() { - counterCubit = MockCounterCubit(); - }); - - testWidgets('renders current count', (tester) async { - const state = 42; - when(() => counterCubit.state).thenReturn(state); - await tester.pumpApp( - BlocProvider.value( - value: counterCubit, - child: const CounterView(), - ), - ); - expect(find.text('$state'), findsOneWidget); - }); - - testWidgets('calls increment when increment button is tapped', - (tester) async { - when(() => counterCubit.state).thenReturn(0); - when(() => counterCubit.increment()).thenReturn(null); - await tester.pumpApp( - BlocProvider.value( - value: counterCubit, - child: const CounterView(), - ), - ); - await tester.tap(find.byIcon(Icons.add)); - verify(() => counterCubit.increment()).called(1); - }); - - testWidgets('calls decrement when decrement button is tapped', - (tester) async { - when(() => counterCubit.state).thenReturn(0); - when(() => counterCubit.decrement()).thenReturn(null); - await tester.pumpApp( - BlocProvider.value( - value: counterCubit, - child: const CounterView(), - ), - ); - await tester.tap(find.byIcon(Icons.remove)); - verify(() => counterCubit.decrement()).called(1); - }); - }); }