From 19bfd035cca5657ef1c35d86a8f95c75fb98ab20 Mon Sep 17 00:00:00 2001 From: arturplaczek Date: Fri, 22 Apr 2022 11:38:25 +0200 Subject: [PATCH] feat: create score widgets --- lib/game/view/widgets/score_ball.dart | 59 ++++++++++++ lib/game/view/widgets/score_view.dart | 85 ++++++++++++++++ lib/game/view/widgets/widgets.dart | 2 + test/game/view/widgets/score_ball_test.dart | 101 ++++++++++++++++++++ test/game/view/widgets/score_view_test.dart | 86 +++++++++++++++++ 5 files changed, 333 insertions(+) create mode 100644 lib/game/view/widgets/score_ball.dart create mode 100644 lib/game/view/widgets/score_view.dart create mode 100644 test/game/view/widgets/score_ball_test.dart create mode 100644 test/game/view/widgets/score_view_test.dart diff --git a/lib/game/view/widgets/score_ball.dart b/lib/game/view/widgets/score_ball.dart new file mode 100644 index 00000000..816b7dac --- /dev/null +++ b/lib/game/view/widgets/score_ball.dart @@ -0,0 +1,59 @@ +// 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/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; + +class ScoreBalls extends StatelessWidget { + const ScoreBalls({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final balls = context.select((GameBloc bloc) => bloc.state.balls); + + return Row( + children: [ + Text( + l10n.ballCt, + style: AppTextStyle.subtitle1.copyWith( + color: AppColors.orange, + ), + ), + const SizedBox(width: 8), + Row( + children: [ + ScoreBall(isActive: balls >= 1), + ScoreBall(isActive: balls >= 2), + ScoreBall(isActive: balls >= 3), + ], + ), + ], + ); + } +} + +@visibleForTesting +class ScoreBall extends StatelessWidget { + const ScoreBall({ + Key? key, + required this.isActive, + }) : super(key: key); + + final bool isActive; + + @override + Widget build(BuildContext context) { + final color = isActive ? AppColors.orange : AppColors.orange.withAlpha(128); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Container( + color: color, + height: 8, + width: 8, + ), + ); + } +} diff --git a/lib/game/view/widgets/score_view.dart b/lib/game/view/widgets/score_view.dart new file mode 100644 index 00000000..d3c04f3c --- /dev/null +++ b/lib/game/view/widgets/score_view.dart @@ -0,0 +1,85 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; + +class ScoreView extends StatelessWidget { + const ScoreView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: AnimatedSwitcher( + duration: kThemeAnimationDuration, + child: isGameOver ? const _GameOver() : const _ScoreWidget(), + ), + ); + } +} + +class _GameOver extends StatelessWidget { + const _GameOver({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Text( + l10n.gameOver, + style: AppTextStyle.headline1.copyWith( + color: AppColors.white, + ), + ); + } +} + +class _ScoreWidget extends StatelessWidget { + const _ScoreWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.score.toLowerCase(), + style: AppTextStyle.subtitle1.copyWith( + color: AppColors.orange, + ), + ), + const _ScoreText(), + const ScoreBalls(), + ], + ); + } +} + +class _ScoreText extends StatelessWidget { + const _ScoreText({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final score = context.select((GameBloc bloc) => bloc.state.score); + final numberFormatter = NumberFormat.decimalPattern('en_US'); + final formattedScore = numberFormatter.format(score).replaceAll(',', '.'); + + return Text( + formattedScore, + style: AppTextStyle.headline1.copyWith( + color: AppColors.white, + ), + ); + } +} diff --git a/lib/game/view/widgets/widgets.dart b/lib/game/view/widgets/widgets.dart index 674577af..aef21da4 100644 --- a/lib/game/view/widgets/widgets.dart +++ b/lib/game/view/widgets/widgets.dart @@ -1,3 +1,5 @@ export 'bonus_animation.dart'; export 'game_hud.dart'; export 'play_button_overlay.dart'; +export 'score_ball.dart'; +export 'score_view.dart'; diff --git a/test/game/view/widgets/score_ball_test.dart b/test/game/view/widgets/score_ball_test.dart new file mode 100644 index 00000000..af8a0111 --- /dev/null +++ b/test/game/view/widgets/score_ball_test.dart @@ -0,0 +1,101 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/theme/app_colors.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + group('ScoreBalls renders', () { + late GameBloc gameBloc; + const initialState = GameState( + score: 0, + balls: 3, + bonusHistory: [], + ); + + setUp(() { + gameBloc = MockGameBloc(); + + whenListen( + gameBloc, + Stream.value(initialState), + initialState: initialState, + ); + }); + + testWidgets('three active balls', (tester) async { + await tester.pumpApp( + const ScoreBalls(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.byType(ScoreBall), findsNWidgets(3)); + }); + + testWidgets('two active balls', (tester) async { + final state = initialState.copyWith( + balls: 2, + ); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp( + const ScoreBalls(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is ScoreBall && widget.isActive, + ), + findsNWidgets(2), + ); + + expect( + find.byWidgetPredicate( + (widget) => widget is ScoreBall && !widget.isActive, + ), + findsOneWidget, + ); + }); + }); + + testWidgets('active score ball is displaying with proper color', + (tester) async { + await tester.pumpApp( + const ScoreBall(isActive: true), + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => widget is Container && widget.color == AppColors.orange, + ), + findsOneWidget, + ); + }); + + testWidgets('inactive score ball is displaying with proper color', + (tester) async { + await tester.pumpApp( + const ScoreBall(isActive: false), + ); + await tester.pump(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is Container && + widget.color == AppColors.orange.withAlpha(128), + ), + findsOneWidget, + ); + }); +} diff --git a/test/game/view/widgets/score_view_test.dart b/test/game/view/widgets/score_view_test.dart new file mode 100644 index 00000000..03a20e46 --- /dev/null +++ b/test/game/view/widgets/score_view_test.dart @@ -0,0 +1,86 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/intl.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + late GameBloc gameBloc; + late StreamController stateController; + const score = 123456789; + const initialState = GameState( + score: score, + balls: 1, + bonusHistory: [], + ); + + setUp(() { + gameBloc = MockGameBloc(); + stateController = StreamController()..add(initialState); + + whenListen( + gameBloc, + stateController.stream, + initialState: initialState, + ); + }); + + String _formatScore(int score) { + final numberFormatter = NumberFormat.decimalPattern('en_US'); + return numberFormatter.format(score).replaceAll(',', '.'); + } + + group('ScoreView', () { + testWidgets('renders score', (tester) async { + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.text(_formatScore(score)), findsOneWidget); + }); + + testWidgets('renders game over', (tester) async { + final l10n = await AppLocalizations.delegate.load(const Locale('en')); + + stateController.add( + initialState.copyWith( + balls: 0, + ), + ); + + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + await tester.pump(); + + expect(find.text(l10n.gameOver), findsOneWidget); + }); + + testWidgets('updates the score', (tester) async { + await tester.pumpApp( + const ScoreView(), + gameBloc: gameBloc, + ); + + expect(find.text('$score'), findsOneWidget); + + final newState = initialState.copyWith( + score: 987654321, + ); + + stateController.add(newState); + + await tester.pump(); + + expect(find.text(_formatScore(newState.score)), findsOneWidget); + }); + }); +}