diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index bd03f02a..d57d144b 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -40,8 +40,8 @@ "@character": { "description": "Text displayed on the leaders board page header character column" }, - "userName": "UserName", - "@userName": { + "username": "Username", + "@username": { "description": "Text displayed on the leaders board page header userName column" }, "score": "Score", diff --git a/lib/leaderboard/view/leaderboard_page.dart b/lib/leaderboard/view/leaderboard_page.dart index 8a031ceb..1a7bf1b1 100644 --- a/lib/leaderboard/view/leaderboard_page.dart +++ b/lib/leaderboard/view/leaderboard_page.dart @@ -8,14 +8,9 @@ import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/theme/theme.dart'; import 'package:pinball_theme/pinball_theme.dart'; -/// {@template leaderboard_page} -/// Shows the leaderboard page of [Competitor]s. -/// {@endtemplate} class LeaderboardPage extends StatelessWidget { - /// {@macro leaderboard_page} const LeaderboardPage({Key? key, required this.theme}) : super(key: key); - /// Current [CharacterTheme] to customize screen final CharacterTheme theme; static Route route({required CharacterTheme theme}) { @@ -29,7 +24,7 @@ class LeaderboardPage extends StatelessWidget { return BlocProvider( create: (context) => LeaderboardBloc( context.read(), - ), + )..add(const Top10Fetched()), child: LeaderboardView(theme: theme), ); } @@ -56,7 +51,21 @@ class LeaderboardView extends StatelessWidget { style: Theme.of(context).textTheme.headline3, ), const SizedBox(height: 80), - _LeaderboardRanking(theme: theme), + BlocBuilder( + builder: (context, state) { + switch (state.status) { + case LeaderboardStatus.loading: + return _LeaderboardLoading(theme: theme); + case LeaderboardStatus.success: + return _LeaderboardRanking( + ranking: state.leaderboard, + theme: theme, + ); + case LeaderboardStatus.error: + return _LeaderboardError(theme: theme); + } + }, + ), const SizedBox(height: 20), TextButton( onPressed: () => Navigator.of(context).push( @@ -72,9 +81,45 @@ class LeaderboardView extends StatelessWidget { } } +class _LeaderboardLoading extends StatelessWidget { + const _LeaderboardLoading({Key? key, required this.theme}) : super(key: key); + + final CharacterTheme theme; + + @override + Widget build(BuildContext context) { + return const Center( + child: CircularProgressIndicator(), + ); + } +} + +class _LeaderboardError extends StatelessWidget { + const _LeaderboardError({Key? key, required this.theme}) : super(key: key); + + final CharacterTheme theme; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20), + child: Text( + 'There was en error loading data!', + style: + Theme.of(context).textTheme.headline6?.copyWith(color: Colors.red), + ), + ); + } +} + class _LeaderboardRanking extends StatelessWidget { - const _LeaderboardRanking({Key? key, required this.theme}) : super(key: key); + const _LeaderboardRanking({ + Key? key, + required this.ranking, + required this.theme, + }) : super(key: key); + final List ranking; final CharacterTheme theme; @override @@ -85,7 +130,10 @@ class _LeaderboardRanking extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ _LeaderboardHeaders(theme: theme), - _LeaderboardList(theme: theme), + _LeaderboardList( + ranking: ranking, + theme: theme, + ), ], ), ); @@ -106,7 +154,7 @@ class _LeaderboardHeaders extends StatelessWidget { children: [ _LeaderboardHeaderItem(title: l10n.rank, theme: theme), _LeaderboardHeaderItem(title: l10n.character, theme: theme), - _LeaderboardHeaderItem(title: l10n.userName, theme: theme), + _LeaderboardHeaderItem(title: l10n.username, theme: theme), _LeaderboardHeaderItem(title: l10n.score, theme: theme), ], ); @@ -140,8 +188,13 @@ class _LeaderboardHeaderItem extends StatelessWidget { } class _LeaderboardList extends StatelessWidget { - const _LeaderboardList({Key? key, required this.theme}) : super(key: key); + const _LeaderboardList({ + Key? key, + required this.ranking, + required this.theme, + }) : super(key: key); + final List ranking; final CharacterTheme theme; @override @@ -159,7 +212,7 @@ class _LeaderboardList extends StatelessWidget { ), theme: theme, ), - itemCount: 10, + itemCount: ranking.length, ); } } diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 5a6a249f..f8415a58 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -7,11 +7,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:pinball/app/app.dart'; import 'package:pinball/landing/landing.dart'; -class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} +import '../../helpers/mocks.dart'; void main() { group('App', () { diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index e1bd8a0c..e189f4dd 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -1,9 +1,12 @@ +import 'package:bloc_test/bloc_test.dart'; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/theme/theme.dart'; class MockPinballGame extends Mock implements PinballGame {} @@ -32,6 +35,11 @@ class MockGameState extends Mock implements GameState {} class MockThemeCubit extends Mock implements ThemeCubit {} +class MockLeaderboardBloc extends MockBloc + implements LeaderboardBloc {} + +class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} + class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index e0b953d2..6abb1376 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.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'; @@ -22,26 +23,34 @@ extension PumpApp on WidgetTester { MockNavigator? navigator, GameBloc? gameBloc, ThemeCubit? themeCubit, + LeaderboardRepository? leaderboardRepository, }) { return pumpWidget( - MultiBlocProvider( + MultiRepositoryProvider( providers: [ - BlocProvider.value( - value: themeCubit ?? MockThemeCubit(), - ), - BlocProvider.value( - value: gameBloc ?? MockGameBloc(), - ), + RepositoryProvider.value( + value: leaderboardRepository ?? MockLeaderboardRepository(), + ) ], - child: MaterialApp( - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, + child: MultiBlocProvider( + providers: [ + BlocProvider.value( + value: themeCubit ?? MockThemeCubit(), + ), + BlocProvider.value( + value: gameBloc ?? MockGameBloc(), + ), ], - supportedLocales: AppLocalizations.supportedLocales, - home: navigator != null - ? MockNavigatorProvider(navigator: navigator, child: widget) - : widget, + child: MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: navigator != null + ? MockNavigatorProvider(navigator: navigator, child: widget) + : widget, + ), ), ), ); diff --git a/test/leaderboard/view/leaderboard_page_test.dart b/test/leaderboard/view/leaderboard_page_test.dart index 3a96b9f8..d46cb515 100644 --- a/test/leaderboard/view/leaderboard_page_test.dart +++ b/test/leaderboard/view/leaderboard_page_test.dart @@ -1,10 +1,13 @@ // 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'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/leaderboard/view/leaderboard_page.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -33,11 +36,22 @@ void main() { }); group('LeaderboardView', () { + late LeaderboardBloc leaderboardBloc; + + setUp(() { + leaderboardBloc = MockLeaderboardBloc(); + }); + testWidgets('renders correctly', (tester) async { final l10n = await AppLocalizations.delegate.load(Locale('en')); + when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial()); + await tester.pumpApp( - LeaderboardPage( - theme: DashTheme(), + BlocProvider.value( + value: leaderboardBloc, + child: LeaderboardView( + theme: DashTheme(), + ), ), ); @@ -45,6 +59,78 @@ void main() { expect(find.text(l10n.retry), findsOneWidget); }); + testWidgets('renders loading view when bloc emits [loading]', + (tester) async { + when(() => leaderboardBloc.state).thenReturn(LeaderboardState.initial()); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: LeaderboardView( + theme: DashTheme(), + ), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + expect(find.text('There was en error loading data!'), findsNothing); + expect(find.byType(ListView), findsNothing); + }); + + testWidgets('renders error view when bloc emits [error]', (tester) async { + when(() => leaderboardBloc.state).thenReturn( + LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: LeaderboardView( + theme: DashTheme(), + ), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsNothing); + expect(find.text('There was en error loading data!'), findsOneWidget); + expect(find.byType(ListView), findsNothing); + }); + + testWidgets('renders success view when bloc emits [success]', + (tester) async { + final l10n = await AppLocalizations.delegate.load(Locale('en')); + when(() => leaderboardBloc.state).thenReturn( + LeaderboardState( + status: LeaderboardStatus.success, + ranking: LeaderboardRanking(ranking: 0, outOf: 0), + leaderboard: const [ + LeaderboardEntry( + playerInitials: 'ABC', + score: 10000, + character: CharacterType.dash, + ), + ], + ), + ); + + await tester.pumpApp( + BlocProvider.value( + value: leaderboardBloc, + child: LeaderboardView( + theme: DashTheme(), + ), + ), + ); + + expect(find.byType(CircularProgressIndicator), findsNothing); + expect(find.text('There was en error loading data!'), findsNothing); + expect(find.text(l10n.rank), findsOneWidget); + expect(find.text(l10n.character), findsOneWidget); + expect(find.text(l10n.username), findsOneWidget); + expect(find.text(l10n.score), findsOneWidget); + expect(find.byType(ListView), findsOneWidget); + }); + testWidgets('navigates to CharacterSelectionPage when retry is tapped', (tester) async { final navigator = MockNavigator();