From 75feeaae26b92d4151ba64dc6b5e6b2fc34859b1 Mon Sep 17 00:00:00 2001 From: Erick Date: Sat, 7 May 2022 18:23:41 -0300 Subject: [PATCH 1/2] feat: adding leaderboard error screen (#398) * feat: adding leaderboard error screen * Update lib/game/components/backbox/displays/leaderboard_failure_display.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/components/backbox/backbox.dart | 2 + .../components/backbox/displays/displays.dart | 1 + .../displays/leaderboard_failure_display.dart | 23 +++++++ lib/l10n/arb/app_en.arb | 4 ++ .../game/components/backbox/backbox_test.dart | 25 ++++++++ .../leaderboard_failure_display_test.dart | 60 +++++++++++++++++++ test/game/pinball_game_test.dart | 5 +- test/game/view/pinball_game_page_test.dart | 5 +- 8 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 lib/game/components/backbox/displays/leaderboard_failure_display.dart create mode 100644 test/game/components/backbox/displays/leaderboard_failure_display_test.dart diff --git a/lib/game/components/backbox/backbox.dart b/lib/game/components/backbox/backbox.dart index 69f68a58..1c2fde6f 100644 --- a/lib/game/components/backbox/backbox.dart +++ b/lib/game/components/backbox/backbox.dart @@ -65,6 +65,8 @@ class Backbox extends PositionComponent with ZIndex, HasGameRef { _display.add(LoadingDisplay()); } else if (state is LeaderboardSuccessState) { _display.add(LeaderboardDisplay(entries: state.entries)); + } else if (state is LeaderboardFailureState) { + _display.add(LeaderboardFailureDisplay()); } else if (state is InitialsFormState) { if (_platformHelper.isMobile) { gameRef.overlays.add(PinballGame.mobileControlsOverlay); diff --git a/lib/game/components/backbox/displays/displays.dart b/lib/game/components/backbox/displays/displays.dart index 4dda7048..06b0a4e3 100644 --- a/lib/game/components/backbox/displays/displays.dart +++ b/lib/game/components/backbox/displays/displays.dart @@ -2,4 +2,5 @@ export 'initials_input_display.dart'; export 'initials_submission_failure_display.dart'; export 'initials_submission_success_display.dart'; export 'leaderboard_display.dart'; +export 'leaderboard_failure_display.dart'; export 'loading_display.dart'; diff --git a/lib/game/components/backbox/displays/leaderboard_failure_display.dart b/lib/game/components/backbox/displays/leaderboard_failure_display.dart new file mode 100644 index 00000000..a519f9e2 --- /dev/null +++ b/lib/game/components/backbox/displays/leaderboard_failure_display.dart @@ -0,0 +1,23 @@ +import 'package:flame/components.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template leaderboard_failure_display} +/// Display showing an error message when the leaderboard couldn't be loaded +/// {@endtemplate} +class LeaderboardFailureDisplay extends Component { + /// {@macro leaderboard_failure_display} + LeaderboardFailureDisplay(); + + @override + Future onLoad() async { + final l10n = readProvider(); + await add( + ErrorComponent( + label: l10n.leaderboardErrorMessage, + position: Vector2(0, -18), + ), + ); + } +} diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 2d30d03a..b46a42f4 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -151,5 +151,9 @@ "enter": "Enter", "@enter": { "description": "Text shown on the mobile controls enter button" + }, + "leaderboardErrorMessage": "No connection. Leaderboard and sharing functionality is unavailable.", + "@leaderboardErrorMessage": { + "description": "Text shown when the leaderboard had an error while loading" } } diff --git a/test/game/components/backbox/backbox_test.dart b/test/game/components/backbox/backbox_test.dart index 24cb4fb2..5c42ea79 100644 --- a/test/game/components/backbox/backbox_test.dart +++ b/test/game/components/backbox/backbox_test.dart @@ -99,6 +99,9 @@ class _MockAppLocalizations extends Mock implements AppLocalizations { @override String get loading => ''; + + @override + String get leaderboardErrorMessage => ''; } void main() { @@ -310,6 +313,28 @@ void main() { }, ); + flameTester.test( + 'adds LeaderboardFailureDisplay on LeaderboardFailureState', + (game) async { + whenListen( + bloc, + Stream.empty(), + initialState: LeaderboardFailureState(), + ); + + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); + await game.pump(backbox); + + expect( + game.descendants().whereType().length, + equals(1), + ); + }, + ); + flameTester.test( 'closes the subscription when it is removed', (game) async { diff --git a/test/game/components/backbox/displays/leaderboard_failure_display_test.dart b/test/game/components/backbox/displays/leaderboard_failure_display_test.dart new file mode 100644 index 00000000..8ce4c839 --- /dev/null +++ b/test/game/components/backbox/displays/leaderboard_failure_display_test.dart @@ -0,0 +1,60 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/components/backbox/displays/displays.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + await super.onLoad(); + images.prefix = ''; + await images.loadAll( + [ + Assets.images.errorBackground.keyName, + ], + ); + } + + Future pump(LeaderboardFailureDisplay component) { + return ensureAdd( + FlameProvider.value( + _MockAppLocalizations(), + children: [component], + ), + ); + } +} + +class _MockAppLocalizations extends Mock implements AppLocalizations { + @override + String get leaderboardErrorMessage => 'Message'; +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('LeaderboardFailureDisplay', () { + final flameTester = FlameTester(_TestGame.new); + + flameTester.test('renders correctly', (game) async { + await game.pump(LeaderboardFailureDisplay()); + + expect( + game + .descendants() + .where( + (component) => + component is TextComponent && component.text == 'Message', + ) + .length, + equals(1), + ); + }); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index b05e349f..ba54939c 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -57,7 +57,10 @@ class _TestDebugPinballGame extends DebugPinballGame { class _MockGameBloc extends Mock implements GameBloc {} -class _MockAppLocalizations extends Mock implements AppLocalizations {} +class _MockAppLocalizations extends Mock implements AppLocalizations { + @override + String get leaderboardErrorMessage => ''; +} class _MockEventPosition extends Mock implements EventPosition {} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 18dbeec7..b2cde26e 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -50,7 +50,10 @@ class _MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {} class _MockStartGameBloc extends Mock implements StartGameBloc {} -class _MockAppLocalizations extends Mock implements AppLocalizations {} +class _MockAppLocalizations extends Mock implements AppLocalizations { + @override + String get leaderboardErrorMessage => ''; +} class _MockPinballAudioPlayer extends Mock implements PinballAudioPlayer {} From 6aef28ed8c3cbc27791a49fc32333af60c20085e Mon Sep 17 00:00:00 2001 From: Erick Date: Sat, 7 May 2022 19:08:03 -0300 Subject: [PATCH 2/2] feat: final screen for the initials submission error (#393) --- lib/game/components/backbox/backbox.dart | 13 +++- .../components/backbox/bloc/backbox_bloc.dart | 7 +- .../backbox/bloc/backbox_state.dart | 13 +++- .../initials_submission_failure_display.dart | 34 ++++++++-- lib/l10n/arb/app_en.arb | 8 +++ .../game/components/backbox/backbox_test.dart | 49 +++++++++++++- .../backbox/bloc/backbox_bloc_test.dart | 2 +- .../backbox/bloc/backbox_state_test.dart | 49 +++++++++++++- ...tials_submission_failure_display_test.dart | 66 +++++++++++++++++-- 9 files changed, 221 insertions(+), 20 deletions(-) diff --git a/lib/game/components/backbox/backbox.dart b/lib/game/components/backbox/backbox.dart index 1c2fde6f..e3b935fe 100644 --- a/lib/game/components/backbox/backbox.dart +++ b/lib/game/components/backbox/backbox.dart @@ -89,7 +89,18 @@ class Backbox extends PositionComponent with ZIndex, HasGameRef { } else if (state is InitialsSuccessState) { _display.add(InitialsSubmissionSuccessDisplay()); } else if (state is InitialsFailureState) { - _display.add(InitialsSubmissionFailureDisplay()); + _display.add( + InitialsSubmissionFailureDisplay( + onDismissed: () { + _bloc.add( + PlayerInitialsRequested( + score: state.score, + character: state.character, + ), + ); + }, + ), + ); } } diff --git a/lib/game/components/backbox/bloc/backbox_bloc.dart b/lib/game/components/backbox/bloc/backbox_bloc.dart index 7afd34de..8b155d57 100644 --- a/lib/game/components/backbox/bloc/backbox_bloc.dart +++ b/lib/game/components/backbox/bloc/backbox_bloc.dart @@ -56,7 +56,12 @@ class BackboxBloc extends Bloc { emit(InitialsSuccessState()); } catch (error, stackTrace) { addError(error, stackTrace); - emit(InitialsFailureState()); + emit( + InitialsFailureState( + score: event.score, + character: event.character, + ), + ); } } diff --git a/lib/game/components/backbox/bloc/backbox_state.dart b/lib/game/components/backbox/bloc/backbox_state.dart index 482bb298..29a0718a 100644 --- a/lib/game/components/backbox/bloc/backbox_state.dart +++ b/lib/game/components/backbox/bloc/backbox_state.dart @@ -62,6 +62,17 @@ class InitialsSuccessState extends BackboxState { /// State when the initials submission failed. class InitialsFailureState extends BackboxState { + const InitialsFailureState({ + required this.score, + required this.character, + }); + + /// Player's score. + final int score; + + /// Player's character. + final CharacterTheme character; + @override - List get props => []; + List get props => [score, character]; } diff --git a/lib/game/components/backbox/displays/initials_submission_failure_display.dart b/lib/game/components/backbox/displays/initials_submission_failure_display.dart index 4cc5a9f5..a9bd84c5 100644 --- a/lib/game/components/backbox/displays/initials_submission_failure_display.dart +++ b/lib/game/components/backbox/displays/initials_submission_failure_display.dart @@ -1,27 +1,47 @@ import 'package:flame/components.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_ui/pinball_ui.dart'; final _bodyTextPaint = TextPaint( style: const TextStyle( - fontSize: 3, + fontSize: 1.8, color: PinballColors.white, fontFamily: PinballFonts.pixeloidSans, + fontWeight: FontWeight.w400, ), ); /// {@template initials_submission_failure_display} /// [Backbox] display for when a failure occurs during initials submission. /// {@endtemplate} -class InitialsSubmissionFailureDisplay extends TextComponent { +class InitialsSubmissionFailureDisplay extends Component { + /// {@macro initials_submission_failure_display} + InitialsSubmissionFailureDisplay({ + required this.onDismissed, + }); + + final VoidCallback onDismissed; + @override Future onLoad() async { - await super.onLoad(); - position = Vector2(0, -10); - anchor = Anchor.center; - text = 'Failure!'; - textRenderer = _bodyTextPaint; + final l10n = readProvider(); + + await addAll([ + ErrorComponent.bold( + label: l10n.initialsErrorTitle, + position: Vector2(0, -20), + ), + TextComponent( + text: l10n.initialsErrorMessage, + anchor: Anchor.center, + position: Vector2(0, -12), + textRenderer: _bodyTextPaint, + ), + TimerComponent(period: 4, onTick: onDismissed), + ]); } } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b46a42f4..fd633efe 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -152,6 +152,14 @@ "@enter": { "description": "Text shown on the mobile controls enter button" }, + "initialsErrorTitle": "Uh-oh... well, that didn’t work", + "@enter": { + "description": "Title shown when the initials submission fails" + }, + "initialsErrorMessage": "Please try a different combination of letters", + "@initialsErrorMessage": { + "description": "Message on shown when the initials submission fails" + }, "leaderboardErrorMessage": "No connection. Leaderboard and sharing functionality is unavailable.", "@leaderboardErrorMessage": { "description": "Text shown when the leaderboard had an error while loading" diff --git a/test/game/components/backbox/backbox_test.dart b/test/game/components/backbox/backbox_test.dart index 5c42ea79..1416f84e 100644 --- a/test/game/components/backbox/backbox_test.dart +++ b/test/game/components/backbox/backbox_test.dart @@ -100,6 +100,12 @@ class _MockAppLocalizations extends Mock implements AppLocalizations { @override String get loading => ''; + @override + String get initialsErrorTitle => ''; + + @override + String get initialsErrorMessage => ''; + @override String get leaderboardErrorMessage => ''; } @@ -273,7 +279,10 @@ void main() { whenListen( bloc, Stream.empty(), - initialState: InitialsFailureState(), + initialState: InitialsFailureState( + score: 0, + character: theme.DashTheme(), + ), ); final backbox = Backbox.test( bloc: bloc, @@ -354,7 +363,12 @@ void main() { backbox.removeFromParent(); await game.ready(); - streamController.add(InitialsFailureState()); + streamController.add( + InitialsFailureState( + score: 10, + character: theme.DashTheme(), + ), + ); await game.ready(); expect( @@ -366,5 +380,36 @@ void main() { ); }, ); + + flameTester.test( + 'adds PlayerInitialsSubmitted when the timer is finished', + (game) async { + final initialState = InitialsFailureState( + score: 10, + character: theme.DashTheme(), + ); + whenListen( + bloc, + Stream.fromIterable([]), + initialState: initialState, + ); + + final backbox = Backbox.test( + bloc: bloc, + platformHelper: platformHelper, + ); + await game.pump(backbox); + game.update(4); + + verify( + () => bloc.add( + PlayerInitialsRequested( + score: 10, + character: theme.DashTheme(), + ), + ), + ).called(1); + }, + ); }); } diff --git a/test/game/components/backbox/bloc/backbox_bloc_test.dart b/test/game/components/backbox/bloc/backbox_bloc_test.dart index 0f265875..e260b42d 100644 --- a/test/game/components/backbox/bloc/backbox_bloc_test.dart +++ b/test/game/components/backbox/bloc/backbox_bloc_test.dart @@ -113,7 +113,7 @@ void main() { ), expect: () => [ LoadingState(), - InitialsFailureState(), + InitialsFailureState(score: 10, character: DashTheme()), ], ); }); diff --git a/test/game/components/backbox/bloc/backbox_state_test.dart b/test/game/components/backbox/bloc/backbox_state_test.dart index dd262408..dfd3792b 100644 --- a/test/game/components/backbox/bloc/backbox_state_test.dart +++ b/test/game/components/backbox/bloc/backbox_state_test.dart @@ -124,11 +124,56 @@ void main() { group('InitialsFailureState', () { test('can be instantiated', () { - expect(InitialsFailureState(), isNotNull); + expect( + InitialsFailureState( + score: 10, + character: AndroidTheme(), + ), + isNotNull, + ); }); test('supports value comparison', () { - expect(InitialsFailureState(), equals(InitialsFailureState())); + expect( + InitialsFailureState( + score: 10, + character: AndroidTheme(), + ), + equals( + InitialsFailureState( + score: 10, + character: AndroidTheme(), + ), + ), + ); + expect( + InitialsFailureState( + score: 10, + character: AndroidTheme(), + ), + isNot( + equals( + InitialsFailureState( + score: 12, + character: AndroidTheme(), + ), + ), + ), + ); + expect( + InitialsFailureState( + score: 10, + character: AndroidTheme(), + ), + isNot( + equals( + InitialsFailureState( + score: 10, + character: DashTheme(), + ), + ), + ), + ); }); }); }); diff --git a/test/game/components/backbox/displays/initials_submission_failure_display_test.dart b/test/game/components/backbox/displays/initials_submission_failure_display_test.dart index b37b41e7..f0eb03c6 100644 --- a/test/game/components/backbox/displays/initials_submission_failure_display_test.dart +++ b/test/game/components/backbox/displays/initials_submission_failure_display_test.dart @@ -4,18 +4,74 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/components/backbox/displays/initials_submission_failure_display.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball_components/gen/assets.gen.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class _TestGame extends Forge2DGame { + @override + Future onLoad() async { + await super.onLoad(); + images.prefix = ''; + await images.loadAll( + [ + Assets.images.errorBackground.keyName, + ], + ); + } + + Future pump(InitialsSubmissionFailureDisplay component) { + return ensureAdd( + FlameProvider.value( + _MockAppLocalizations(), + children: [component], + ), + ); + } +} + +class _MockAppLocalizations extends Mock implements AppLocalizations { + @override + String get initialsErrorTitle => 'Title'; + + @override + String get initialsErrorMessage => 'Message'; +} void main() { + TestWidgetsFlutterBinding.ensureInitialized(); group('InitialsSubmissionFailureDisplay', () { - final flameTester = FlameTester(Forge2DGame.new); + final flameTester = FlameTester(_TestGame.new); flameTester.test('renders correctly', (game) async { - await game.ensureAdd(InitialsSubmissionFailureDisplay()); + await game.pump( + InitialsSubmissionFailureDisplay( + onDismissed: () {}, + ), + ); - final component = game.firstChild(); - expect(component, isNotNull); - expect(component?.text, equals('Failure!')); + expect( + game + .descendants() + .where( + (component) => + component is TextComponent && component.text == 'Title', + ) + .length, + equals(1), + ); + expect( + game + .descendants() + .where( + (component) => + component is TextComponent && component.text == 'Message', + ) + .length, + equals(1), + ); }); }); }