feat: score widget with animation (#206)
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 306 KiB |
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
After Width: | Height: | Size: 11 KiB |
@ -1,46 +1,122 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:pinball/game/game.dart';
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball/gen/gen.dart';
|
||||||
|
import 'package:pinball/theme/app_colors.dart';
|
||||||
|
|
||||||
/// {@template game_hud}
|
/// {@template game_hud}
|
||||||
/// Overlay of a [PinballGame] that displays the current [GameState.score] and
|
/// Overlay on the [PinballGame].
|
||||||
/// [GameState.balls].
|
///
|
||||||
|
/// Displays the current [GameState.score], [GameState.balls] and animates when
|
||||||
|
/// the player gets a [GameBonus].
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class GameHud extends StatelessWidget {
|
class GameHud extends StatefulWidget {
|
||||||
/// {@macro game_hud}
|
/// {@macro game_hud}
|
||||||
const GameHud({Key? key}) : super(key: key);
|
const GameHud({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GameHud> createState() => _GameHudState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GameHudState extends State<GameHud> {
|
||||||
|
bool showAnimation = false;
|
||||||
|
|
||||||
|
/// Ratio from sprite frame (width 500, height 144) w / h = ratio
|
||||||
|
static const _ratio = 3.47;
|
||||||
|
static const _width = 265.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final state = context.watch<GameBloc>().state;
|
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver);
|
||||||
|
|
||||||
return Container(
|
return _ScoreViewDecoration(
|
||||||
color: Colors.redAccent,
|
child: SizedBox(
|
||||||
width: 200,
|
height: _width / _ratio,
|
||||||
height: 100,
|
width: _width,
|
||||||
padding: const EdgeInsets.all(16),
|
child: BlocListener<GameBloc, GameState>(
|
||||||
child: Row(
|
listenWhen: (previous, current) =>
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
previous.bonusHistory.length != current.bonusHistory.length,
|
||||||
children: [
|
listener: (_, __) => setState(() => showAnimation = true),
|
||||||
Text(
|
child: AnimatedSwitcher(
|
||||||
'${state.score}',
|
duration: kThemeAnimationDuration,
|
||||||
style: Theme.of(context).textTheme.headline3,
|
child: showAnimation && !isGameOver
|
||||||
),
|
? _AnimationView(
|
||||||
Wrap(
|
onComplete: () {
|
||||||
direction: Axis.vertical,
|
if (mounted) {
|
||||||
children: [
|
setState(() => showAnimation = false);
|
||||||
for (var i = 0; i < state.balls; i++)
|
}
|
||||||
const Padding(
|
},
|
||||||
padding: EdgeInsets.only(top: 6, right: 6),
|
)
|
||||||
child: CircleAvatar(
|
: const ScoreView(),
|
||||||
radius: 8,
|
),
|
||||||
backgroundColor: Colors.black,
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ScoreViewDecoration extends StatelessWidget {
|
||||||
|
const _ScoreViewDecoration({
|
||||||
|
Key? key,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const radius = BorderRadius.all(Radius.circular(12));
|
||||||
|
const boardWidth = 5.0;
|
||||||
|
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: radius,
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.white,
|
||||||
|
width: boardWidth,
|
||||||
|
),
|
||||||
|
image: DecorationImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: AssetImage(
|
||||||
|
Assets.images.score.miniScoreBackground.path,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(boardWidth - 1),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: radius,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimationView extends StatelessWidget {
|
||||||
|
const _AnimationView({
|
||||||
|
Key? key,
|
||||||
|
required this.onComplete,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final VoidCallback onComplete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final lastBonus = context.select(
|
||||||
|
(GameBloc bloc) => bloc.state.bonusHistory.last,
|
||||||
|
);
|
||||||
|
switch (lastBonus) {
|
||||||
|
case GameBonus.dashNest:
|
||||||
|
return BonusAnimation.dashNest(onCompleted: onComplete);
|
||||||
|
case GameBonus.sparkyTurboCharge:
|
||||||
|
return BonusAnimation.sparkyTurboCharge(onCompleted: onComplete);
|
||||||
|
case GameBonus.dinoChomp:
|
||||||
|
return BonusAnimation.dinoChomp(onCompleted: onComplete);
|
||||||
|
case GameBonus.googleWord:
|
||||||
|
return BonusAnimation.googleWord(onCompleted: onComplete);
|
||||||
|
case GameBonus.androidSpaceship:
|
||||||
|
return BonusAnimation.androidSpaceship(onCompleted: onComplete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/// {@template round_count_display}
|
||||||
|
/// Colored square indicating if a round is available.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class RoundCountDisplay extends StatelessWidget {
|
||||||
|
/// {@macro round_count_display}
|
||||||
|
const RoundCountDisplay({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
// TODO(arturplaczek): refactor when GameState handle balls and rounds and
|
||||||
|
// select state.rounds property instead of state.ball
|
||||||
|
final balls = context.select((GameBloc bloc) => bloc.state.balls);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.rounds,
|
||||||
|
style: AppTextStyle.subtitle1.copyWith(
|
||||||
|
color: AppColors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
RoundIndicator(isActive: balls >= 1),
|
||||||
|
RoundIndicator(isActive: balls >= 2),
|
||||||
|
RoundIndicator(isActive: balls >= 3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template round_indicator}
|
||||||
|
/// [Widget] that displays the round indicator.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@visibleForTesting
|
||||||
|
class RoundIndicator extends StatelessWidget {
|
||||||
|
/// {@macro round_indicator}
|
||||||
|
const RoundIndicator({
|
||||||
|
Key? key,
|
||||||
|
required this.isActive,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// A value that describes whether the indicator is active.
|
||||||
|
final bool isActive;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = isActive ? AppColors.orange : AppColors.orange.withAlpha(128);
|
||||||
|
const size = 8.0;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Container(
|
||||||
|
color: color,
|
||||||
|
height: size,
|
||||||
|
width: size,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
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';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
/// {@template score_view}
|
||||||
|
/// [Widget] that displays the score.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class ScoreView extends StatelessWidget {
|
||||||
|
/// {@macro score_view}
|
||||||
|
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 _ScoreDisplay(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _ScoreDisplay extends StatelessWidget {
|
||||||
|
const _ScoreDisplay({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.score.toLowerCase(),
|
||||||
|
style: AppTextStyle.subtitle1.copyWith(
|
||||||
|
color: AppColors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const _ScoreText(),
|
||||||
|
const RoundCountDisplay(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
score.formatScore(),
|
||||||
|
style: AppTextStyle.headline1.copyWith(
|
||||||
|
color: AppColors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
export 'bonus_animation.dart';
|
export 'bonus_animation.dart';
|
||||||
export 'game_hud.dart';
|
export 'game_hud.dart';
|
||||||
export 'play_button_overlay.dart';
|
export 'play_button_overlay.dart';
|
||||||
|
export 'round_count_display.dart';
|
||||||
|
export 'score_view.dart';
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export 'assets.gen.dart';
|
export 'assets.gen.dart';
|
||||||
|
export 'fonts.gen.dart';
|
||||||
export 'pinball_fonts.dart';
|
export 'pinball_fonts.dart';
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
// ignore_for_file: prefer_const_constructors
|
|
||||||
|
|
||||||
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 '../../helpers/helpers.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('GameHud', () {
|
|
||||||
late GameBloc gameBloc;
|
|
||||||
const initialState = GameState(
|
|
||||||
score: 10,
|
|
||||||
balls: 2,
|
|
||||||
bonusHistory: [],
|
|
||||||
);
|
|
||||||
|
|
||||||
void _mockState(GameState state) {
|
|
||||||
whenListen(
|
|
||||||
gameBloc,
|
|
||||||
Stream.value(state),
|
|
||||||
initialState: state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _pumpHud(WidgetTester tester) async {
|
|
||||||
await tester.pumpApp(
|
|
||||||
GameHud(),
|
|
||||||
gameBloc: gameBloc,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
gameBloc = MockGameBloc();
|
|
||||||
_mockState(initialState);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
'renders the current score',
|
|
||||||
(tester) async {
|
|
||||||
await _pumpHud(tester);
|
|
||||||
expect(find.text(initialState.score.toString()), findsOneWidget);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
'renders the current ball number',
|
|
||||||
(tester) async {
|
|
||||||
await _pumpHud(tester);
|
|
||||||
expect(
|
|
||||||
find.byType(CircleAvatar),
|
|
||||||
findsNWidgets(initialState.balls),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets('updates the score', (tester) async {
|
|
||||||
await _pumpHud(tester);
|
|
||||||
expect(find.text(initialState.score.toString()), findsOneWidget);
|
|
||||||
|
|
||||||
_mockState(initialState.copyWith(score: 20));
|
|
||||||
|
|
||||||
await tester.pump();
|
|
||||||
expect(find.text('20'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('updates the ball number', (tester) async {
|
|
||||||
await _pumpHud(tester);
|
|
||||||
expect(
|
|
||||||
find.byType(CircleAvatar),
|
|
||||||
findsNWidgets(initialState.balls),
|
|
||||||
);
|
|
||||||
|
|
||||||
_mockState(initialState.copyWith(balls: 1));
|
|
||||||
|
|
||||||
await tester.pump();
|
|
||||||
expect(
|
|
||||||
find.byType(CircleAvatar),
|
|
||||||
findsNWidgets(1),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -0,0 +1,137 @@
|
|||||||
|
// 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_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
import 'package:pinball/l10n/l10n.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
import '../../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GameHud', () {
|
||||||
|
late GameBloc gameBloc;
|
||||||
|
|
||||||
|
const initialState = GameState(
|
||||||
|
score: 1000,
|
||||||
|
balls: 2,
|
||||||
|
bonusHistory: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
gameBloc = MockGameBloc();
|
||||||
|
await Future.wait<void>(BonusAnimation.loadAssets());
|
||||||
|
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
Stream.value(initialState),
|
||||||
|
initialState: initialState,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// We cannot use pumpApp when we are testing animation because
|
||||||
|
// animation tests needs to be run and check in tester.runAsync
|
||||||
|
Future<void> _pumpAppWithWidget(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
localizationsDelegates: const [
|
||||||
|
AppLocalizations.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
home: Scaffold(
|
||||||
|
body: BlocProvider<GameBloc>.value(
|
||||||
|
value: gameBloc,
|
||||||
|
child: GameHud(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('renders ScoreView widget', () {
|
||||||
|
testWidgets(
|
||||||
|
'with the score',
|
||||||
|
(tester) async {
|
||||||
|
await tester.pumpApp(
|
||||||
|
GameHud(),
|
||||||
|
gameBloc: gameBloc,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text(initialState.score.formatScore()), findsOneWidget);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'on game over',
|
||||||
|
(tester) async {
|
||||||
|
final state = initialState.copyWith(
|
||||||
|
bonusHistory: [GameBonus.dashNest],
|
||||||
|
balls: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
Stream.value(state),
|
||||||
|
initialState: initialState,
|
||||||
|
);
|
||||||
|
await tester.pumpApp(
|
||||||
|
GameHud(),
|
||||||
|
gameBloc: gameBloc,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(ScoreView), findsOneWidget);
|
||||||
|
expect(find.byType(BonusAnimation), findsNothing);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final gameBonus in GameBonus.values) {
|
||||||
|
testWidgets('renders BonusAnimation for $gameBonus', (tester) async {
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final state = initialState.copyWith(
|
||||||
|
bonusHistory: [gameBonus],
|
||||||
|
);
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
Stream.value(state),
|
||||||
|
initialState: initialState,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _pumpAppWithWidget(tester);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.byType(BonusAnimation), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'goes back to ScoreView after the animation',
|
||||||
|
(tester) async {
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final state = initialState.copyWith(
|
||||||
|
bonusHistory: [GameBonus.dashNest],
|
||||||
|
);
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
Stream.value(state),
|
||||||
|
initialState: initialState,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _pumpAppWithWidget(tester);
|
||||||
|
await tester.pump();
|
||||||
|
// TODO(arturplaczek): remove magic number once this is merged:
|
||||||
|
// https://github.com/flame-engine/flame/pull/1564
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 4));
|
||||||
|
|
||||||
|
await expectLater(find.byType(ScoreView), findsOneWidget);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
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('RoundCountDisplay 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 round indicator', (tester) async {
|
||||||
|
await tester.pumpApp(
|
||||||
|
const RoundCountDisplay(),
|
||||||
|
gameBloc: gameBloc,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.byType(RoundIndicator), findsNWidgets(3));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('two active round indicator', (tester) async {
|
||||||
|
final state = initialState.copyWith(
|
||||||
|
balls: 2,
|
||||||
|
);
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
Stream.value(state),
|
||||||
|
initialState: state,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpApp(
|
||||||
|
const RoundCountDisplay(),
|
||||||
|
gameBloc: gameBloc,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.byWidgetPredicate(
|
||||||
|
(widget) => widget is RoundIndicator && widget.isActive,
|
||||||
|
),
|
||||||
|
findsNWidgets(2),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.byWidgetPredicate(
|
||||||
|
(widget) => widget is RoundIndicator && !widget.isActive,
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('one active round indicator', (tester) async {
|
||||||
|
final state = initialState.copyWith(
|
||||||
|
balls: 1,
|
||||||
|
);
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
Stream.value(state),
|
||||||
|
initialState: state,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpApp(
|
||||||
|
const RoundCountDisplay(),
|
||||||
|
gameBloc: gameBloc,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.byWidgetPredicate(
|
||||||
|
(widget) => widget is RoundIndicator && widget.isActive,
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.byWidgetPredicate(
|
||||||
|
(widget) => widget is RoundIndicator && !widget.isActive,
|
||||||
|
),
|
||||||
|
findsNWidgets(2),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('active round indicator is displaying with proper color',
|
||||||
|
(tester) async {
|
||||||
|
await tester.pumpApp(
|
||||||
|
const RoundIndicator(isActive: true),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.byWidgetPredicate(
|
||||||
|
(widget) => widget is Container && widget.color == AppColors.orange,
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('inactive round indicator is displaying with proper color',
|
||||||
|
(tester) async {
|
||||||
|
await tester.pumpApp(
|
||||||
|
const RoundIndicator(isActive: false),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is Container &&
|
||||||
|
widget.color == AppColors.orange.withAlpha(128),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
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/l10n/l10n.dart';
|
||||||
|
import 'package:pinball_components/pinball_components.dart';
|
||||||
|
|
||||||
|
import '../../../helpers/helpers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late GameBloc gameBloc;
|
||||||
|
late StreamController<GameState> stateController;
|
||||||
|
const score = 123456789;
|
||||||
|
const initialState = GameState(
|
||||||
|
score: score,
|
||||||
|
balls: 1,
|
||||||
|
bonusHistory: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
gameBloc = MockGameBloc();
|
||||||
|
stateController = StreamController<GameState>()..add(initialState);
|
||||||
|
|
||||||
|
whenListen(
|
||||||
|
gameBloc,
|
||||||
|
stateController.stream,
|
||||||
|
initialState: initialState,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('ScoreView', () {
|
||||||
|
testWidgets('renders score', (tester) async {
|
||||||
|
await tester.pumpApp(
|
||||||
|
const ScoreView(),
|
||||||
|
gameBloc: gameBloc,
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text(score.formatScore()), 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.formatScore()), findsOneWidget);
|
||||||
|
|
||||||
|
final newState = initialState.copyWith(
|
||||||
|
score: 987654321,
|
||||||
|
);
|
||||||
|
|
||||||
|
stateController.add(newState);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text(newState.score.formatScore()), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|