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..d0a63673 --- /dev/null +++ b/lib/start_game/widgets/start_game_listener.dart @@ -0,0 +1,90 @@ +// ignore_for_file: public_member_api_docs + +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'; + +class StartGameListener extends StatelessWidget { + 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/start_game/widgets/start_game_listener_test.dart b/test/start_game/widgets/start_game_listener_test.dart new file mode 100644 index 00000000..3f6d2c14 --- /dev/null +++ b/test/start_game/widgets/start_game_listener_test.dart @@ -0,0 +1,275 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/flame.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 'package:pinball_theme/pinball_theme.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 { + Flame.images.prefix = ''; + await Flame.images.load(const DashTheme().animation.keyName); + await Flame.images.load(const AndroidTheme().animation.keyName); + await Flame.images.load(const DinoTheme().animation.keyName); + await Flame.images.load(const SparkyTheme().animation.keyName); + + 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); + }, + ); + }); + }); +}