diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 579d830b..0fa6a1ad 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -70,7 +70,10 @@ class _PinballGameViewState extends State { showDialog( context: context, builder: (_) { - return GameOverDialog(theme: widget.theme.characterTheme); + return GameOverDialog( + score: state.score, + theme: widget.theme.characterTheme, + ); }, ); } diff --git a/lib/game/view/widgets/game_over_dialog.dart b/lib/game/view/widgets/game_over_dialog.dart index 29164a62..e3c5a1e1 100644 --- a/lib/game/view/widgets/game_over_dialog.dart +++ b/lib/game/view/widgets/game_over_dialog.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/leaderboard/leaderboard.dart'; @@ -9,34 +11,162 @@ import 'package:pinball_theme/pinball_theme.dart'; /// {@endtemplate} class GameOverDialog extends StatelessWidget { /// {@macro game_over_dialog} - const GameOverDialog({Key? key, required this.theme}) : super(key: key); + const GameOverDialog({Key? key, required this.score, required this.theme}) + : super(key: key); - /// Current [CharacterTheme] to customize dialog + /// Score achieved by the current user. + final int score; + + /// Theme of the current user. + final CharacterTheme theme; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LeaderboardBloc( + context.read(), + ), + child: GameOverDialogView(score: score, theme: theme), + ); + } +} + +/// {@template game_over_dialog_view} +/// View for showing final score when the game is finished. +/// {@endtemplate} +@visibleForTesting +class GameOverDialogView extends StatefulWidget { + /// {@macro game_over_dialog_view} + const GameOverDialogView({ + Key? key, + required this.score, + required this.theme, + }) : super(key: key); + + /// Score achieved by the current user. + final int score; + + /// Theme of the current user. final CharacterTheme theme; + @override + State createState() => _GameOverDialogViewState(); +} + +class _GameOverDialogViewState extends State { + final playerInitialsInputController = TextEditingController(); + + @override + void dispose() { + playerInitialsInputController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; + // TODO(ruimiguel): refactor this view once UI design finished. return Dialog( child: SizedBox( width: 200, - height: 200, + height: 250, child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(l10n.gameOver), - TextButton( - onPressed: () => Navigator.of(context).push( - LeaderboardPage.route(theme: theme), - ), - child: Text(l10n.leaderboard), + child: Padding( + padding: const EdgeInsets.all(10), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.gameOver, + style: Theme.of(context).textTheme.headline4, + ), + const SizedBox( + height: 20, + ), + Text( + '${l10n.yourScore} ${widget.score}', + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox( + height: 15, + ), + TextField( + key: const Key('player_initials_text_field'), + controller: playerInitialsInputController, + textCapitalization: TextCapitalization.characters, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: l10n.enterInitials, + ), + maxLength: 3, + ), + const SizedBox( + height: 10, + ), + _GameOverDialogActions( + score: widget.score, + theme: widget.theme, + playerInitialsInputController: + playerInitialsInputController, + ), + ], ), - ], + ), ), ), ), ); } } + +class _GameOverDialogActions extends StatelessWidget { + const _GameOverDialogActions({ + Key? key, + required this.score, + required this.theme, + required this.playerInitialsInputController, + }) : super(key: key); + + final int score; + final CharacterTheme theme; + final TextEditingController playerInitialsInputController; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return BlocBuilder( + builder: (context, state) { + switch (state.status) { + case LeaderboardStatus.loading: + return TextButton( + onPressed: () { + context.read().add( + LeaderboardEntryAdded( + entry: LeaderboardEntryData( + playerInitials: + playerInitialsInputController.text.toUpperCase(), + score: score, + character: theme.toType, + ), + ), + ); + }, + child: Text(l10n.addUser), + ); + case LeaderboardStatus.success: + return TextButton( + onPressed: () => Navigator.of(context).push( + LeaderboardPage.route(theme: theme), + ), + child: Text(l10n.leaderboard), + ); + case LeaderboardStatus.error: + return Text(l10n.error); + } + }, + ); + } +} diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 235c8f2e..aa56e015 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -51,5 +51,21 @@ "retry": "Retry", "@retry": { "description": "Text displayed on the retry button leaders board page" + }, + "addUser": "Add User", + "@addUser": { + "description": "Text displayed on the add user button at ending dialog" + }, + "error": "Error", + "@error": { + "description": "Text displayed on the ending dialog when there is any error on sending user" + }, + "yourScore": "Your score is", + "@yourScore": { + "description": "Text displayed on the ending dialog when game finishes to show the final score" + }, + "enterInitials": "Enter your initials", + "@enterInitials": { + "description": "Text displayed on the ending dialog when game finishes to ask the user for his initials" } } \ No newline at end of file diff --git a/test/game/view/widgets/game_over_dialog_test.dart b/test/game/view/widgets/game_over_dialog_test.dart index 8150bcd5..814a7a45 100644 --- a/test/game/view/widgets/game_over_dialog_test.dart +++ b/test/game/view/widgets/game_over_dialog_test.dart @@ -1,44 +1,195 @@ // ignore_for_file: prefer_const_constructors +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball_theme/pinball_theme.dart'; import '../../../helpers/helpers.dart'; void main() { group('GameOverDialog', () { - testWidgets('renders correctly', (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); + testWidgets('renders GameOverDialogView', (tester) async { await tester.pumpApp( - const GameOverDialog( + GameOverDialog( + score: 1000, theme: DashTheme(), ), ); - expect(find.text(l10n.gameOver), findsOneWidget); - expect(find.text(l10n.leaderboard), findsOneWidget); + expect(find.byType(GameOverDialogView), findsOneWidget); }); - testWidgets('tapping on leaderboard button navigates to LeaderBoardPage', - (tester) async { - final l10n = await AppLocalizations.delegate.load(Locale('en')); - final navigator = MockNavigator(); - when(() => navigator.push(any())).thenAnswer((_) async {}); + group('GameOverDialogView', () { + late LeaderboardBloc leaderboardBloc; - await tester.pumpApp( - const GameOverDialog( - theme: DashTheme(), + final leaderboard = [ + LeaderboardEntry( + rank: '1', + playerInitials: 'ABC', + score: 5000, + character: DashTheme().characterAsset, ), - navigator: navigator, + ]; + final entryData = LeaderboardEntryData( + playerInitials: 'VGV', + score: 10000, + character: CharacterType.dash, ); - await tester.tap(find.widgetWithText(TextButton, l10n.leaderboard)); + setUp(() { + leaderboardBloc = MockLeaderboardBloc(); + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: const LeaderboardState.initial(), + ); + }); + + testWidgets('renders input text view when bloc emits [loading]', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: GameOverDialogView( + score: entryData.score, + theme: entryData.character.toTheme, + ), + ), + ); + + expect(find.widgetWithText(TextButton, l10n.addUser), findsOneWidget); + }); + + testWidgets('renders error view when bloc emits [error]', (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState.initial() + .copyWith(status: LeaderboardStatus.error), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: GameOverDialogView( + score: entryData.score, + theme: entryData.character.toTheme, + ), + ), + ); + + expect(find.text(l10n.error), findsOneWidget); + }); + + testWidgets('renders success view when bloc emits [success]', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState( + status: LeaderboardStatus.success, + ranking: LeaderboardRanking(ranking: 1, outOf: 2), + leaderboard: leaderboard, + ), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: GameOverDialogView( + score: entryData.score, + theme: entryData.character.toTheme, + ), + ), + ); + + expect( + find.widgetWithText(TextButton, l10n.leaderboard), + findsOneWidget, + ); + }); + + testWidgets('adds LeaderboardEntryAdded when tap on add user button', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState.initial(), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: GameOverDialogView( + score: entryData.score, + theme: entryData.character.toTheme, + ), + ), + ); + + await tester.enterText( + find.byKey(const Key('player_initials_text_field')), + entryData.playerInitials, + ); + + final button = find.widgetWithText(TextButton, l10n.addUser); + await tester.ensureVisible(button); + await tester.tap(button); + + verify( + () => leaderboardBloc.add(LeaderboardEntryAdded(entry: entryData)), + ).called(1); + }); + + testWidgets('navigates to LeaderboardPage when tap on leaderboard button', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + final navigator = MockNavigator(); + when(() => navigator.push(any())).thenAnswer((_) async {}); + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState( + status: LeaderboardStatus.success, + ranking: LeaderboardRanking(ranking: 1, outOf: 2), + leaderboard: leaderboard, + ), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: GameOverDialogView( + score: entryData.score, + theme: entryData.character.toTheme, + ), + ), + navigator: navigator, + ); + + final button = find.widgetWithText(TextButton, l10n.leaderboard); + await tester.ensureVisible(button); + await tester.tap(button); - verify(() => navigator.push(any())).called(1); + verify(() => navigator.push(any())).called(1); + }); }); }); } diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index c658c531..206b25a4 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -1,4 +1,3 @@ -import 'package:bloc_test/bloc_test.dart'; import 'package:flame/components.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; @@ -39,8 +38,7 @@ class MockGameState extends Mock implements GameState {} class MockThemeCubit extends Mock implements ThemeCubit {} -class MockLeaderboardBloc extends MockBloc - implements LeaderboardBloc {} +class MockLeaderboardBloc extends Mock implements LeaderboardBloc {} class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} diff --git a/test/leaderboard/view/leaderboard_page_test.dart b/test/leaderboard/view/leaderboard_page_test.dart index 9460818d..4221d727 100644 --- a/test/leaderboard/view/leaderboard_page_test.dart +++ b/test/leaderboard/view/leaderboard_page_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: prefer_const_constructors +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -42,7 +43,11 @@ void main() { testWidgets('renders correctly', (tester) async { final l10n = await AppLocalizations.delegate.load(Locale('en')); - when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial()); + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState.initial(), + ); await tester.pumpApp( BlocProvider.value( @@ -59,7 +64,11 @@ void main() { testWidgets('renders loading view when bloc emits [loading]', (tester) async { - when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial()); + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState.initial(), + ); await tester.pumpApp( BlocProvider.value( @@ -76,8 +85,12 @@ void main() { }); testWidgets('renders error view when bloc emits [error]', (tester) async { - when(() => leaderboardBloc.state).thenReturn( - LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState.initial().copyWith( + status: LeaderboardStatus.error, + ), ); await tester.pumpApp( @@ -97,8 +110,10 @@ void main() { testWidgets('renders success view when bloc emits [success]', (tester) async { final l10n = await AppLocalizations.delegate.load(Locale('en')); - when(() => leaderboardBloc.state).thenReturn( - LeaderboardState( + whenListen( + leaderboardBloc, + const Stream.empty(), + initialState: LeaderboardState( status: LeaderboardStatus.success, ranking: LeaderboardRanking(ranking: 0, outOf: 0), leaderboard: [