From 03f60fbffe7659ca0114bd159e2604cd185c7efb Mon Sep 17 00:00:00 2001 From: arturplaczek <33895544+arturplaczek@users.noreply.github.com> Date: Wed, 4 May 2022 20:18:39 +0200 Subject: [PATCH] chore: restore GameHud visibility and control game flow (#247) --- lib/app/view/app.dart | 8 +- lib/game/view/pinball_game_page.dart | 56 ++-- .../view/widgets/play_button_overlay.dart | 17 +- .../widgets/how_to_play_dialog.dart | 30 +- .../view/character_selection_page.dart | 13 +- lib/start_game/bloc/start_game_bloc.dart | 10 +- lib/start_game/start_game.dart | 1 + .../widgets/start_game_listener.dart | 95 +++++++ test/game/view/pinball_game_page_test.dart | 11 +- .../widgets/play_button_overlay_test.dart | 50 +--- test/how_to_play/how_to_play_dialog_test.dart | 50 ++-- .../view/character_selection_page_test.dart | 37 +-- .../start_game/bloc/start_game_bloc_test.dart | 12 +- .../widgets/start_game_listener_test.dart | 269 ++++++++++++++++++ 14 files changed, 481 insertions(+), 178 deletions(-) create mode 100644 lib/start_game/widgets/start_game_listener.dart create mode 100644 test/start_game/widgets/start_game_listener_test.dart diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index d778b55b..de46512b 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -8,6 +8,7 @@ import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_ui/pinball_ui.dart'; @@ -34,8 +35,11 @@ class App extends StatelessWidget { RepositoryProvider.value(value: _leaderboardRepository), RepositoryProvider.value(value: _pinballAudio), ], - child: BlocProvider( - create: (context) => CharacterThemeCubit(), + child: MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => CharacterThemeCubit()), + BlocProvider(create: (_) => StartGameBloc()), + ], child: MaterialApp( title: 'I/O Pinball', theme: PinballTheme.standard, diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 3a626ba4..21b9ac5c 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -60,7 +60,6 @@ class PinballGamePage extends StatelessWidget { return MultiBlocProvider( providers: [ - BlocProvider(create: (_) => StartGameBloc(game: game)), BlocProvider(create: (_) => GameBloc()), BlocProvider(create: (_) => AssetsManagerCubit(loadables)..load()), ], @@ -105,36 +104,43 @@ class PinballGameLoadedView extends StatelessWidget { @override Widget build(BuildContext context) { + final isPlaying = context.select( + (StartGameBloc bloc) => bloc.state.status == StartGameStatus.play, + ); final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; final screenWidth = MediaQuery.of(context).size.width; final leftMargin = (screenWidth / 2) - (gameWidgetWidth / 1.8); - return Stack( - children: [ - Positioned.fill( - child: GameWidget( - game: game, - initialActiveOverlays: const [PinballGame.playButtonOverlay], - overlayBuilderMap: { - PinballGame.playButtonOverlay: (context, game) { - return Positioned( - bottom: 20, - right: 0, - left: 0, - child: PlayButtonOverlay(game: game), - ); + return StartGameListener( + game: game, + child: Stack( + children: [ + Positioned.fill( + child: GameWidget( + game: game, + initialActiveOverlays: const [PinballGame.playButtonOverlay], + overlayBuilderMap: { + PinballGame.playButtonOverlay: (context, game) { + return const Positioned( + bottom: 20, + right: 0, + left: 0, + child: PlayButtonOverlay(), + ); + }, }, - }, + ), ), - ), - // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc - // status - Positioned( - top: 16, - left: leftMargin, - child: const GameHud(), - ), - ], + Positioned( + top: 16, + left: leftMargin, + child: Visibility( + visible: isPlaying, + child: const GameHud(), + ), + ), + ], + ), ); } } diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index 1d4a10fb..7a954c77 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:pinball/game/pinball_game.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_ui/pinball_ui.dart'; /// {@template play_button_overlay} @@ -9,13 +9,7 @@ import 'package:pinball_ui/pinball_ui.dart'; /// {@endtemplate} class PlayButtonOverlay extends StatelessWidget { /// {@macro play_button_overlay} - const PlayButtonOverlay({ - Key? key, - required PinballGame game, - }) : _game = game, - super(key: key); - - final PinballGame _game; + const PlayButtonOverlay({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -23,9 +17,8 @@ class PlayButtonOverlay extends StatelessWidget { return PinballButton( text: l10n.play, - onTap: () async { - _game.gameFlowController.start(); - await showCharacterSelectionDialog(context); + onTap: () { + context.read().add(const PlayTapped()); }, ); } diff --git a/lib/how_to_play/widgets/how_to_play_dialog.dart b/lib/how_to_play/widgets/how_to_play_dialog.dart index e91698f5..426fcbe5 100644 --- a/lib/how_to_play/widgets/how_to_play_dialog.dart +++ b/lib/how_to_play/widgets/how_to_play_dialog.dart @@ -3,10 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/gen/gen.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_ui/pinball_ui.dart'; import 'package:platform_helper/platform_helper.dart'; @@ -51,24 +49,16 @@ extension on Control { } } -Future showHowToPlayDialog(BuildContext context) { - final audio = context.read(); - return showDialog( - context: context, - builder: (_) => HowToPlayDialog(), - ).then((_) { - audio.ioPinballVoiceOver(); - }); -} - class HowToPlayDialog extends StatefulWidget { HowToPlayDialog({ Key? key, + required this.onDismissCallback, @visibleForTesting PlatformHelper? platformHelper, }) : platformHelper = platformHelper ?? PlatformHelper(), super(key: key); final PlatformHelper platformHelper; + final VoidCallback onDismissCallback; @override State createState() => _HowToPlayDialogState(); @@ -82,6 +72,7 @@ class _HowToPlayDialogState extends State { closeTimer = Timer(const Duration(seconds: 3), () { if (mounted) { Navigator.of(context).pop(); + widget.onDismissCallback.call(); } }); } @@ -96,10 +87,17 @@ class _HowToPlayDialogState extends State { Widget build(BuildContext context) { final isMobile = widget.platformHelper.isMobile; final l10n = context.l10n; - return PinballDialog( - title: l10n.howToPlay, - subtitle: l10n.tipsForFlips, - child: isMobile ? const _MobileBody() : const _DesktopBody(), + + return WillPopScope( + onWillPop: () { + widget.onDismissCallback.call(); + return Future.value(true); + }, + child: PinballDialog( + title: l10n.howToPlay, + subtitle: l10n.tipsForFlips, + child: isMobile ? const _MobileBody() : const _DesktopBody(), + ), ); } } diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 3b00829b..be671dd1 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -1,20 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; -/// Inflates [CharacterSelectionDialog] using [showDialog]. -Future showCharacterSelectionDialog(BuildContext context) { - return showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const CharacterSelectionDialog(), - ); -} - /// {@template character_selection_dialog} /// Dialog used to select the playing character of the game. /// {@endtemplate character_selection_dialog} @@ -59,7 +50,7 @@ class _SelectCharacterButton extends StatelessWidget { return PinballButton( onTap: () async { Navigator.of(context).pop(); - await showHowToPlayDialog(context); + context.read().add(const CharacterSelected()); }, text: l10n.select, ); diff --git a/lib/start_game/bloc/start_game_bloc.dart b/lib/start_game/bloc/start_game_bloc.dart index ba44d88c..3a96b57b 100644 --- a/lib/start_game/bloc/start_game_bloc.dart +++ b/lib/start_game/bloc/start_game_bloc.dart @@ -1,6 +1,5 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:pinball/game/game.dart'; part 'start_game_event.dart'; part 'start_game_state.dart'; @@ -10,23 +9,16 @@ part 'start_game_state.dart'; /// {@endtemplate} class StartGameBloc extends Bloc { /// {@macro start_game_bloc} - StartGameBloc({ - required PinballGame game, - }) : _game = game, - super(const StartGameState.initial()) { + StartGameBloc() : super(const StartGameState.initial()) { on(_onPlayTapped); on(_onCharacterSelected); on(_onHowToPlayFinished); } - final PinballGame _game; - void _onPlayTapped( PlayTapped event, Emitter emit, ) { - _game.gameFlowController.start(); - emit( state.copyWith( status: StartGameStatus.selectCharacter, diff --git a/lib/start_game/start_game.dart b/lib/start_game/start_game.dart index 7171c66d..9e63b170 100644 --- a/lib/start_game/start_game.dart +++ b/lib/start_game/start_game.dart @@ -1 +1,2 @@ export 'bloc/start_game_bloc.dart'; +export 'widgets/start_game_listener.dart'; diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart new file mode 100644 index 00000000..df34b324 --- /dev/null +++ b/lib/start_game/widgets/start_game_listener.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template start_game_listener} +/// Listener that manages the display of dialogs for [StartGameStatus]. +/// +/// It's responsible for starting the game after pressing play button +/// and playing a sound after the 'how to play' dialog. +/// {@endtemplate} +class StartGameListener extends StatelessWidget { + /// {@macro start_game_listener} + const StartGameListener({ + Key? key, + required Widget child, + required PinballGame game, + }) : _child = child, + _game = game, + super(key: key); + + final Widget _child; + final PinballGame _game; + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + switch (state.status) { + case StartGameStatus.initial: + break; + case StartGameStatus.selectCharacter: + _onSelectCharacter(context); + _game.gameFlowController.start(); + break; + case StartGameStatus.howToPlay: + _onHowToPlay(context); + break; + case StartGameStatus.play: + break; + } + }, + child: _child, + ); + } + + void _onSelectCharacter(BuildContext context) { + _showPinballDialog( + context: context, + child: const CharacterSelectionDialog(), + barrierDismissible: false, + ); + } + + void _onHowToPlay(BuildContext context) { + final audio = context.read(); + + _showPinballDialog( + context: context, + child: HowToPlayDialog( + onDismissCallback: () { + context.read().add(const HowToPlayFinished()); + audio.ioPinballVoiceOver(); + }, + ), + ); + } + + void _showPinballDialog({ + required BuildContext context, + required Widget child, + bool barrierDismissible = true, + }) { + final gameWidgetWidth = MediaQuery.of(context).size.height * 9 / 16; + + showDialog( + context: context, + barrierColor: PinballColors.transparent, + barrierDismissible: barrierDismissible, + builder: (_) { + return Center( + child: SizedBox( + height: gameWidgetWidth * 0.87, + width: gameWidgetWidth, + child: child, + ), + ); + }, + ); + } +} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 0ed6e744..90d1b194 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -200,12 +200,11 @@ void main() { find.byWidgetPredicate((w) => w is GameWidget), findsOneWidget, ); - // TODO(arturplaczek): add Visibility to GameHud based on StartGameBloc - // status - // expect( - // find.byType(GameHud), - // findsNothing, - // ); + + expect( + find.byType(GameHud), + findsNothing, + ); }); testWidgets('renders a hud on play state', (tester) async { diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 843592c3..f10c5f5b 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -2,64 +2,44 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/bloc/start_game_bloc.dart'; import '../../../helpers/helpers.dart'; -class _MockPinballGame extends Mock implements PinballGame {} - -class _MockGameFlowController extends Mock implements GameFlowController {} - -class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} +class _MockStartGameBloc extends Mock implements StartGameBloc {} void main() { group('PlayButtonOverlay', () { - late PinballGame game; - late GameFlowController gameFlowController; - late CharacterThemeCubit characterThemeCubit; + late StartGameBloc startGameBloc; setUp(() async { await mockFlameImages(); - - game = _MockPinballGame(); - gameFlowController = _MockGameFlowController(); - characterThemeCubit = _MockCharacterThemeCubit(); + startGameBloc = _MockStartGameBloc(); whenListen( - characterThemeCubit, - const Stream.empty(), - initialState: const CharacterThemeState.initial(), + startGameBloc, + Stream.value(const StartGameState.initial()), + initialState: const StartGameState.initial(), ); - when(() => characterThemeCubit.state) - .thenReturn(const CharacterThemeState.initial()); - when(() => game.gameFlowController).thenReturn(gameFlowController); - when(gameFlowController.start).thenAnswer((_) {}); }); testWidgets('renders correctly', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); - expect(find.text('Play'), findsOneWidget); - }); + await tester.pumpApp(const PlayButtonOverlay()); - testWidgets('calls gameFlowController.start when tapped', (tester) async { - await tester.pumpApp( - PlayButtonOverlay(game: game), - characterThemeCubit: characterThemeCubit, - ); - await tester.tap(find.text('Play')); - await tester.pump(); - verify(gameFlowController.start).called(1); + expect(find.text('Play'), findsOneWidget); }); - testWidgets('displays CharacterSelectionDialog when tapped', + testWidgets('adds PlayTapped event to StartGameBloc when taped', (tester) async { await tester.pumpApp( - PlayButtonOverlay(game: game), - characterThemeCubit: characterThemeCubit, + const PlayButtonOverlay(), + startGameBloc: startGameBloc, ); + await tester.tap(find.text('Play')); await tester.pump(); - expect(find.byType(CharacterSelectionDialog), findsOneWidget); + + verify(() => startGameBloc.add(const PlayTapped())).called(1); }); }); } diff --git a/test/how_to_play/how_to_play_dialog_test.dart b/test/how_to_play/how_to_play_dialog_test.dart index 232aa1d5..c6e60d73 100644 --- a/test/how_to_play/how_to_play_dialog_test.dart +++ b/test/how_to_play/how_to_play_dialog_test.dart @@ -3,13 +3,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/l10n/l10n.dart'; -import 'package:pinball_audio/pinball_audio.dart'; import 'package:platform_helper/platform_helper.dart'; import '../helpers/helpers.dart'; -class _MockPinballAudio extends Mock implements PinballAudio {} - class _MockPlatformHelper extends Mock implements PlatformHelper {} void main() { @@ -25,7 +22,11 @@ void main() { testWidgets( 'can be instantiated without passing in a platform helper', (tester) async { - await tester.pumpApp(HowToPlayDialog()); + await tester.pumpApp( + HowToPlayDialog( + onDismissCallback: () {}, + ), + ); expect(find.byType(HowToPlayDialog), findsOneWidget); }, ); @@ -35,6 +36,7 @@ void main() { await tester.pumpApp( HowToPlayDialog( platformHelper: platformHelper, + onDismissCallback: () {}, ), ); expect(find.text(l10n.howToPlay), findsOneWidget); @@ -49,6 +51,7 @@ void main() { await tester.pumpApp( HowToPlayDialog( platformHelper: platformHelper, + onDismissCallback: () {}, ), ); expect(find.text(l10n.howToPlay), findsOneWidget); @@ -62,7 +65,12 @@ void main() { Builder( builder: (context) { return TextButton( - onPressed: () => showHowToPlayDialog(context), + onPressed: () => showDialog( + context: context, + builder: (_) => HowToPlayDialog( + onDismissCallback: () {}, + ), + ), child: const Text('test'), ); }, @@ -82,7 +90,12 @@ void main() { Builder( builder: (context) { return TextButton( - onPressed: () => showHowToPlayDialog(context), + onPressed: () => showDialog( + context: context, + builder: (_) => HowToPlayDialog( + onDismissCallback: () {}, + ), + ), child: const Text('test'), ); }, @@ -96,30 +109,5 @@ void main() { await tester.pumpAndSettle(); expect(find.byType(HowToPlayDialog), findsNothing); }); - - testWidgets( - 'plays the I/O Pinball voice over audio on dismiss', - (tester) async { - final audio = _MockPinballAudio(); - await tester.pumpApp( - Builder( - builder: (context) { - return TextButton( - onPressed: () => showHowToPlayDialog(context), - child: const Text('test'), - ); - }, - ), - pinballAudio: audio, - ); - expect(find.byType(HowToPlayDialog), findsNothing); - await tester.tap(find.text('test')); - await tester.pumpAndSettle(); - - await tester.tapAt(Offset.zero); - await tester.pumpAndSettle(); - verify(audio.ioPinballVoiceOver).called(1); - }, - ); }); } diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index 7d64dd39..a9c0c7ef 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -2,8 +2,8 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:pinball/how_to_play/how_to_play.dart'; import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; @@ -11,14 +11,19 @@ import '../../helpers/helpers.dart'; class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} +class _MockStartGameBloc extends Mock implements StartGameBloc {} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); late CharacterThemeCubit characterThemeCubit; + late StartGameBloc startGameBloc; setUp(() async { await mockFlameImages(); characterThemeCubit = _MockCharacterThemeCubit(); + startGameBloc = _MockStartGameBloc(); + whenListen( characterThemeCubit, const Stream.empty(), @@ -29,25 +34,6 @@ void main() { }); group('CharacterSelectionDialog', () { - group('showCharacterSelectionDialog', () { - testWidgets('inflates the dialog', (tester) async { - await tester.pumpApp( - Builder( - builder: (context) { - return TextButton( - onPressed: () => showCharacterSelectionDialog(context), - child: const Text('test'), - ); - }, - ), - characterThemeCubit: characterThemeCubit, - ); - await tester.tap(find.text('test')); - await tester.pump(); - expect(find.byType(CharacterSelectionDialog), findsOneWidget); - }); - }); - testWidgets('selecting a new character calls characterSelected on cubit', (tester) async { await tester.pumpApp( @@ -63,15 +49,22 @@ void main() { testWidgets( 'tapping the select button dismisses the character ' - 'dialog and shows the how to play dialog', (tester) async { + 'dialog and calls CharacterSelected event to the bloc', (tester) async { + whenListen( + startGameBloc, + const Stream.empty(), + initialState: const StartGameState.initial(), + ); + await tester.pumpApp( const CharacterSelectionDialog(), characterThemeCubit: characterThemeCubit, + startGameBloc: startGameBloc, ); await tester.tap(find.byType(PinballButton)); await tester.pumpAndSettle(); expect(find.byType(CharacterSelectionDialog), findsNothing); - expect(find.byType(HowToPlayDialog), findsOneWidget); + verify(() => startGameBloc.add(const CharacterSelected())).called(1); }); testWidgets('updating the selected character updates the preview', diff --git a/test/start_game/bloc/start_game_bloc_test.dart b/test/start_game/bloc/start_game_bloc_test.dart index 6663ff4e..ac548a93 100644 --- a/test/start_game/bloc/start_game_bloc_test.dart +++ b/test/start_game/bloc/start_game_bloc_test.dart @@ -24,9 +24,7 @@ void main() { group('StartGameBloc', () { blocTest( 'on PlayTapped changes status to selectCharacter', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const PlayTapped()), expect: () => [ const StartGameState( @@ -37,9 +35,7 @@ void main() { blocTest( 'on CharacterSelected changes status to howToPlay', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const CharacterSelected()), expect: () => [ const StartGameState( @@ -50,9 +46,7 @@ void main() { blocTest( 'on HowToPlayFinished changes status to play', - build: () => StartGameBloc( - game: pinballGame, - ), + build: StartGameBloc.new, act: (bloc) => bloc.add(const HowToPlayFinished()), expect: () => [ const StartGameState( diff --git a/test/start_game/widgets/start_game_listener_test.dart b/test/start_game/widgets/start_game_listener_test.dart new file mode 100644 index 00000000..ca646bc9 --- /dev/null +++ b/test/start_game/widgets/start_game_listener_test.dart @@ -0,0 +1,269 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/how_to_play/how_to_play.dart'; +import 'package:pinball/select_character/select_character.dart'; +import 'package:pinball/start_game/start_game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; + +import '../../helpers/helpers.dart'; + +class _MockStartGameBloc extends Mock implements StartGameBloc {} + +class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} + +class _MockPinballGame extends Mock implements PinballGame {} + +class _MockGameFlowController extends Mock implements GameFlowController {} + +class _MockPinballAudio extends Mock implements PinballAudio {} + +void main() { + late StartGameBloc startGameBloc; + late PinballGame pinballGame; + late PinballAudio pinballAudio; + late CharacterThemeCubit characterThemeCubit; + + group('StartGameListener', () { + setUp(() async { + await mockFlameImages(); + + startGameBloc = _MockStartGameBloc(); + pinballGame = _MockPinballGame(); + pinballAudio = _MockPinballAudio(); + characterThemeCubit = _MockCharacterThemeCubit(); + }); + + group('on selectCharacter status', () { + testWidgets( + 'calls start on the game controller', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.selectCharacter), + ), + initialState: const StartGameState.initial(), + ); + final gameController = _MockGameFlowController(); + when(() => pinballGame.gameFlowController) + .thenAnswer((_) => gameController); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + verify(gameController.start).called(1); + }, + ); + + testWidgets( + 'shows SelectCharacter dialog', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.selectCharacter), + ), + initialState: const StartGameState.initial(), + ); + whenListen( + characterThemeCubit, + Stream.value(const CharacterThemeState.initial()), + initialState: const CharacterThemeState.initial(), + ); + final gameController = _MockGameFlowController(); + when(() => pinballGame.gameFlowController) + .thenAnswer((_) => gameController); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + characterThemeCubit: characterThemeCubit, + ); + + await tester.pump(kThemeAnimationDuration); + + expect( + find.byType(CharacterSelectionDialog), + findsOneWidget, + ); + }, + ); + }); + + testWidgets( + 'on howToPlay status shows HowToPlay dialog', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.howToPlay), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'do nothing on play status', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.play), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + expect( + find.byType(CharacterSelectionDialog), + findsNothing, + ); + }, + ); + + testWidgets( + 'do nothing on initial status', + (tester) async { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.initial), + ), + initialState: const StartGameState.initial(), + ); + + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + expect( + find.byType(CharacterSelectionDialog), + findsNothing, + ); + }, + ); + + group('on dismiss HowToPlayDialog', () { + setUp(() { + whenListen( + startGameBloc, + Stream.value( + const StartGameState(status: StartGameStatus.howToPlay), + ), + initialState: const StartGameState.initial(), + ); + }); + + testWidgets( + 'adds HowToPlayFinished event', + (tester) async { + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + ); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + await tester.tapAt(const Offset(1, 1)); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + await tester.pumpAndSettle(); + + verify( + () => startGameBloc.add(const HowToPlayFinished()), + ).called(1); + }, + ); + + testWidgets( + 'plays the I/O Pinball voice over audio', + (tester) async { + await tester.pumpApp( + StartGameListener( + game: pinballGame, + child: const SizedBox.shrink(), + ), + startGameBloc: startGameBloc, + pinballAudio: pinballAudio, + ); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsOneWidget, + ); + await tester.tapAt(const Offset(1, 1)); + await tester.pumpAndSettle(); + + expect( + find.byType(HowToPlayDialog), + findsNothing, + ); + await tester.pumpAndSettle(); + + verify(pinballAudio.ioPinballVoiceOver).called(1); + }, + ); + }); + }); +}