From 3fbb2564cc1b01d057a286ea38983b10a3f144ff Mon Sep 17 00:00:00 2001 From: Jorge Coca Date: Sun, 1 May 2022 17:11:29 -0500 Subject: [PATCH] feat: character selection for desktop and mobile (#280) --- .../view/widgets/play_button_overlay.dart | 19 +- lib/l10n/arb/app_en.arb | 12 +- .../cubit/character_theme_state.dart | 8 + .../view/character_selection_page.dart | 209 ++++++++++-------- .../widgets/how_to_play_dialog.dart | 32 ++- .../assets/images/button/pinball_button.png | Bin 0 -> 1602 bytes packages/pinball_ui/lib/gen/assets.gen.dart | 9 + packages/pinball_ui/lib/pinball_ui.dart | 1 + .../pinball_ui/lib/src/dialog/dialog.dart | 1 + .../lib/src/dialog/pinball_dialog.dart | 87 ++++++++ .../lib/src/theme/pinball_text_style.dart | 3 +- .../lib/src/widgets/pinball_button.dart | 50 +++++ .../pinball_ui/lib/src/widgets/widgets.dart | 1 + packages/pinball_ui/pubspec.yaml | 1 + .../test/src/dialog/pinball_dialog_test.dart | 44 ++++ .../src/theme/pinball_text_style_test.dart | 4 +- .../test/src/widgets/pinball_button_test.dart | 27 +++ .../widgets/play_button_overlay_test.dart | 30 ++- .../view/character_selection_page_test.dart | 158 +++---------- .../widgets/how_to_play_dialog_test.dart | 25 ++- 20 files changed, 458 insertions(+), 263 deletions(-) create mode 100644 packages/pinball_ui/assets/images/button/pinball_button.png create mode 100644 packages/pinball_ui/lib/src/dialog/pinball_dialog.dart create mode 100644 packages/pinball_ui/lib/src/widgets/pinball_button.dart create mode 100644 packages/pinball_ui/lib/src/widgets/widgets.dart create mode 100644 packages/pinball_ui/test/src/dialog/pinball_dialog_test.dart create mode 100644 packages/pinball_ui/test/src/widgets/pinball_button_test.dart diff --git a/lib/game/view/widgets/play_button_overlay.dart b/lib/game/view/widgets/play_button_overlay.dart index 3db62a50..c855f776 100644 --- a/lib/game/view/widgets/play_button_overlay.dart +++ b/lib/game/view/widgets/play_button_overlay.dart @@ -22,24 +22,9 @@ class PlayButtonOverlay extends StatelessWidget { return Center( child: ElevatedButton( - onPressed: () { + onPressed: () async { _game.gameFlowController.start(); - showDialog( - context: context, - barrierDismissible: false, - builder: (_) { - // TODO(arturplaczek): remove after merge StarBlocListener - final height = MediaQuery.of(context).size.height * 0.5; - - return Center( - child: SizedBox( - height: height, - width: height * 1.4, - child: const CharacterSelectionDialog(), - ), - ); - }, - ); + await showCharacterSelectionDialog(context); }, child: Text(l10n.play), ), diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7691e2dd..562d9b1f 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -46,19 +46,19 @@ }, "select": "Select", "@select": { - "description": "Text displayed on the character selection page select button" + "description": "Text displayed on the character selection dialog - select button" }, "space": "Space", "@space": { "description": "Text displayed on space control button" }, - "characterSelectionTitle": "Choose your character!", + "characterSelectionTitle": "Select a Character", "@characterSelectionTitle": { - "description": "Title text displayed on the character selection page" + "description": "Title text displayed on the character selection dialog" }, - "characterSelectionSubtitle": "There’s no wrong answer", + "characterSelectionSubtitle": "There’s no wrong choice!", "@characterSelectionSubtitle": { - "description": "Text displayed on the selecting character dialog at game beginning" + "description": "Subtitle text displayed on the character selection dialog" }, "gameOver": "Game Over", "@gameOver": { @@ -124,4 +124,4 @@ "@footerGoogleIOText": { "description": "Text shown on the footer which mentions Google I/O" } -} \ No newline at end of file +} diff --git a/lib/select_character/cubit/character_theme_state.dart b/lib/select_character/cubit/character_theme_state.dart index ffe5667c..a1669f69 100644 --- a/lib/select_character/cubit/character_theme_state.dart +++ b/lib/select_character/cubit/character_theme_state.dart @@ -10,6 +10,14 @@ class CharacterThemeState extends Equatable { final CharacterTheme characterTheme; + bool get isSparkySelected => characterTheme == const SparkyTheme(); + + bool get isDashSelected => characterTheme == const DashTheme(); + + bool get isAndroidSelected => characterTheme == const AndroidTheme(); + + bool get isDinoSelected => characterTheme == const DinoTheme(); + @override List get props => [characterTheme]; } diff --git a/lib/select_character/view/character_selection_page.dart b/lib/select_character/view/character_selection_page.dart index 863722e6..864d1b41 100644 --- a/lib/select_character/view/character_selection_page.dart +++ b/lib/select_character/view/character_selection_page.dart @@ -1,139 +1,160 @@ -// ignore_for_file: public_member_api_docs - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/select_character/cubit/character_theme_cubit.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_ui/pinball_ui.dart'; +/// Inflates [CharacterSelectionDialog] using [showDialog]. +Future showCharacterSelectionDialog(BuildContext context) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const CharacterSelectionDialog(), + ); +} + +/// {@template character_selection_dialog} +/// Dialog used to select the playing character of the game. +/// {@endtemplate character_selection_dialog} class CharacterSelectionDialog extends StatelessWidget { + /// {@macro character_selection_dialog} const CharacterSelectionDialog({Key? key}) : super(key: key); - static Route route() { - return MaterialPageRoute( - builder: (_) => const CharacterSelectionDialog(), - ); - } - @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => CharacterThemeCubit(), - child: const CharacterSelectionView(), + final l10n = context.l10n; + return PinballDialog( + title: l10n.characterSelectionTitle, + subtitle: l10n.characterSelectionSubtitle, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Row( + children: [ + Expanded(child: _CharacterPreview()), + Expanded(child: _CharacterGrid()), + ], + ), + ), + const SizedBox(height: 8), + const _SelectCharacterButton(), + ], + ), + ), ); } } -class CharacterSelectionView extends StatelessWidget { - const CharacterSelectionView({Key? key}) : super(key: key); +class _SelectCharacterButton extends StatelessWidget { + const _SelectCharacterButton({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final l10n = context.l10n; + return PinballButton( + onTap: () async { + Navigator.of(context).pop(); + await showHowToPlayDialog(context); + }, + text: l10n.select, + ); + } +} - return PixelatedDecoration( - header: Text( - l10n.characterSelectionTitle, - style: Theme.of(context).textTheme.headline3, - ), - body: SingleChildScrollView( - child: Column( +class _CharacterGrid extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const _CharacterSelectionGridView(), - const SizedBox(height: 20), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - // TODO(arturplaczek): remove after merge StarBlocListener - final height = MediaQuery.of(context).size.height * 0.5; - showDialog( - context: context, - builder: (_) => Center( - child: SizedBox( - height: height, - width: height * 1.4, - child: HowToPlayDialog(), - ), - ), - ); - }, - child: Text(l10n.start), + Column( + children: [ + _Character( + key: const Key('sparky_character_selection'), + character: const SparkyTheme(), + isSelected: state.isSparkySelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('android_character_selection'), + character: const AndroidTheme(), + isSelected: state.isAndroidSelected, + ), + ], + ), + const SizedBox(width: 6), + Column( + children: [ + _Character( + key: const Key('dash_character_selection'), + character: const DashTheme(), + isSelected: state.isDashSelected, + ), + const SizedBox(height: 6), + _Character( + key: const Key('dino_character_selection'), + character: const DinoTheme(), + isSelected: state.isDinoSelected, + ), + ], ), ], - ), - ), + ); + }, ); } } -class _CharacterSelectionGridView extends StatelessWidget { - const _CharacterSelectionGridView({Key? key}) : super(key: key); - +class _CharacterPreview extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20), - child: GridView.count( - shrinkWrap: true, - crossAxisCount: 2, - mainAxisSpacing: 20, - crossAxisSpacing: 20, - children: const [ - CharacterImageButton( - DashTheme(), - key: Key('characterSelectionPage_dashButton'), - ), - CharacterImageButton( - SparkyTheme(), - key: Key('characterSelectionPage_sparkyButton'), - ), - CharacterImageButton( - AndroidTheme(), - key: Key('characterSelectionPage_androidButton'), - ), - CharacterImageButton( - DinoTheme(), - key: Key('characterSelectionPage_dinoButton'), - ), - ], - ), + return BlocBuilder( + builder: (context, state) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + state.characterTheme.name, + style: Theme.of(context).textTheme.headline2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + Expanded(child: state.characterTheme.icon.image()), + ], + ); + }, ); } } -// TODO(allisonryan0002): remove visibility when adding final UI. -@visibleForTesting -class CharacterImageButton extends StatelessWidget { - const CharacterImageButton( - this.characterTheme, { +class _Character extends StatelessWidget { + const _Character({ Key? key, + required this.character, + required this.isSelected, }) : super(key: key); - final CharacterTheme characterTheme; + final CharacterTheme character; + final bool isSelected; @override Widget build(BuildContext context) { - final currentCharacterTheme = - context.select( - (cubit) => cubit.state.characterTheme, - ); - - return GestureDetector( - onTap: () => - context.read().characterSelected(characterTheme), - child: DecoratedBox( - decoration: BoxDecoration( - color: (currentCharacterTheme == characterTheme) - ? Colors.blue.withOpacity(0.5) - : null, - borderRadius: BorderRadius.circular(6), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: characterTheme.icon.image(), + return Expanded( + child: Opacity( + opacity: isSelected ? 1 : 0.3, + child: InkWell( + onTap: () => + context.read().characterSelected(character), + child: character.icon.image(fit: BoxFit.contain), ), ), ); diff --git a/lib/start_game/widgets/how_to_play_dialog.dart b/lib/start_game/widgets/how_to_play_dialog.dart index 500a4288..5d8c68c3 100644 --- a/lib/start_game/widgets/how_to_play_dialog.dart +++ b/lib/start_game/widgets/how_to_play_dialog.dart @@ -50,6 +50,22 @@ extension on Control { } } +Future showHowToPlayDialog(BuildContext context) { + final height = MediaQuery.of(context).size.height * 0.5; + return showDialog( + context: context, + builder: (context) { + return Center( + child: SizedBox( + height: height, + width: height * 1.4, + child: HowToPlayDialog(), + ), + ); + }, + ); +} + class HowToPlayDialog extends StatefulWidget { HowToPlayDialog({ Key? key, @@ -70,7 +86,7 @@ class _HowToPlayDialogState extends State { super.initState(); closeTimer = Timer(const Duration(seconds: 3), () { if (mounted) { - Navigator.of(context).maybePop(); + Navigator.of(context).pop(); } }); } @@ -121,7 +137,10 @@ class _MobileLaunchControls extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final textStyle = Theme.of(context).textTheme.headline3; + final textStyle = Theme.of(context) + .textTheme + .headline3! + .copyWith(color: PinballColors.white); return Column( children: [ Text( @@ -137,7 +156,7 @@ class _MobileLaunchControls extends StatelessWidget { ), TextSpan( text: l10n.launch, - style: textStyle?.copyWith(color: PinballColors.blue), + style: textStyle.copyWith(color: PinballColors.blue), ), ], ), @@ -153,7 +172,10 @@ class _MobileFlipperControls extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final textStyle = Theme.of(context).textTheme.headline3; + final textStyle = Theme.of(context) + .textTheme + .headline3! + .copyWith(color: PinballColors.white); return Column( children: [ Text( @@ -169,7 +191,7 @@ class _MobileFlipperControls extends StatelessWidget { ), TextSpan( text: l10n.flip, - style: textStyle?.copyWith(color: PinballColors.orange), + style: textStyle.copyWith(color: PinballColors.orange), ), ], ), diff --git a/packages/pinball_ui/assets/images/button/pinball_button.png b/packages/pinball_ui/assets/images/button/pinball_button.png new file mode 100644 index 0000000000000000000000000000000000000000..62373b857648faaf527a761891db0916811b6498 GIT binary patch literal 1602 zcmX9;c~p{F6n|fonWAM@npPG)23F>}Ne@>ph=nr-gOc+`VinmRV3$Z48vaj7Y@ zQiLL^DY9@!WHCpWQZds$H5Y0eS7fXVQ52XD`{TX$yT9{$_n!OSz4vmUzmI{QnH~VZ z!1u6s5CBjTg2}o%$lh6fxClAcr5z5*06^bZyC874!W1E)j36H`P$k|rg&3<74jerI z0INiQIc5z2Yx;b>51cp$$%eUSA`&bFpCxrxgXxho{r-a+Xk=f))eEcj(U+j@lVGEt zU!k?>!ukzfg=jzP?M~KC*5B6~qGJjmqst7V%}43YH*v;GnDpeG#j{*LmODkneKg_O zm4&5FU9a4MkK|!6itNgstFQCMm+1K}GR2nBF)%48=TFP;Z`@_q@a4#yx+57>oO>tA zk&lr^NoQVc1Un<-tV;KW;YMr?f7^;R!fohp#AY}ef3tE&*+NyMTm#{V?;fQ}C7nxL zXDIETlUCA}8{>J|u%O_;g!2UlRrFW-L)GHk=W=k{iykxMB8yszo6XF{VfYRaMES5# zwdmZnR&e6(huzK;QO@Fx(`JeZ6P^h;P8hfTfiV}{C3ca7DB=%6j-_u`2yEkUZl-8? zSH@ex&AWF>e5dxqsw7F`Pyilsq+Gng8JJOvm8a&^L!rR$(JPPY|2$##@y|Jx=Rr^6 zc~zpvxi5>$DO629B(tfThmcvI` zI+LN&;4|Ut0S3mv9CHXK3KivuKWYFaNhMgk%6CjKa4c>iE#D((rUQrtqCe^pml5lM zFl|F$abAivm>RlID~ZXG_V?8n%SQ_g6~I*VDKA(4G>hpfF$6-#e$7kP^AroIx(Gz0 z>9SF((J%&Ha)$(_hXzIb`Yfi&X%yKBJt8{YQk?-usq5Sl$s5(mQTkj}O*T9cg$8=< zLX7=wx{j^Axs*a2e+(UI7v_vEIP`-z*OQr6Lxx&K&g69$T>3yTm17Dv-J;Z}r)N7t zP&Z%XDXgf(da)!V-}@96;cfypIch4^@v+>QUJvqj^YJAx_qirL^BSA957^pKc-IK> zzxV3m2vbw3<2J%$^EN&Jb4%&Ee+)d1c;C@^jDrN+<3YN{_F_U;@cQ^WyZnZesS>;csKb}{p~$Zp>P|pAJM|$Q1hO;7*$5`c&T6rGlox3j z$muQLoJ4-l@X@#=7zA=MvDw~spp7BZtOliX=+)7B4;UpnpcHQssR!)J#JmreP9W-* z9<9$7d%n}E-aRR|Yjhq*z8}y<1mjz@Axi_0+xVwcLtxj}9`T97M_1g_3lv1FJNekK zM?-A^^)()AX{c55S&7`D`oMX?bzbS(KV4rLJ9a4O3W>3t`qs zv*k*=DU{z@Nu3Pduwa(9WiYTOOx0UyH2sN-5Uv43^)#Agb zC9Ycvt?-#UpW9oTFW-r6Ha8AZO{C89H-m#Sl7%C^_m#oe+@0V`KTna^`?4<;dWe%} z!}zjQv`ZgJb86M?is{%Ce{)+K`$)nN^E5YXCLe0Q^+1LFu const $AssetsImagesButtonGen(); $AssetsImagesDialogGen get dialog => const $AssetsImagesDialogGen(); } +class $AssetsImagesButtonGen { + const $AssetsImagesButtonGen(); + + /// File path: assets/images/button/pinball_button.png + AssetGenImage get pinballButton => + const AssetGenImage('assets/images/button/pinball_button.png'); +} + class $AssetsImagesDialogGen { const $AssetsImagesDialogGen(); diff --git a/packages/pinball_ui/lib/pinball_ui.dart b/packages/pinball_ui/lib/pinball_ui.dart index 332286ed..eacb5681 100644 --- a/packages/pinball_ui/lib/pinball_ui.dart +++ b/packages/pinball_ui/lib/pinball_ui.dart @@ -6,3 +6,4 @@ export 'package:url_launcher_platform_interface/url_launcher_platform_interface. export 'src/dialog/dialog.dart'; export 'src/external_links/external_links.dart'; export 'src/theme/theme.dart'; +export 'src/widgets/widgets.dart'; diff --git a/packages/pinball_ui/lib/src/dialog/dialog.dart b/packages/pinball_ui/lib/src/dialog/dialog.dart index 7a224272..4731eb5f 100644 --- a/packages/pinball_ui/lib/src/dialog/dialog.dart +++ b/packages/pinball_ui/lib/src/dialog/dialog.dart @@ -1 +1,2 @@ +export 'pinball_dialog.dart'; export 'pixelated_decoration.dart'; diff --git a/packages/pinball_ui/lib/src/dialog/pinball_dialog.dart b/packages/pinball_ui/lib/src/dialog/pinball_dialog.dart new file mode 100644 index 00000000..8ff04754 --- /dev/null +++ b/packages/pinball_ui/lib/src/dialog/pinball_dialog.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template pinball_dialog} +/// Pinball-themed dialog. +/// {@endtemplate} +class PinballDialog extends StatelessWidget { + /// {@macro pinball_dialog} + const PinballDialog({ + Key? key, + required this.title, + required this.child, + this.subtitle, + }) : super(key: key); + + /// Title shown in the dialog. + final String title; + + /// Optional subtitle shown below the [title]. + final String? subtitle; + + /// Body of the dialog. + final Widget child; + + @override + Widget build(BuildContext context) { + final height = MediaQuery.of(context).size.height * 0.5; + return Center( + child: SizedBox( + height: height, + width: height * 1.4, + child: PixelatedDecoration( + header: subtitle != null + ? _TitleAndSubtitle(title: title, subtitle: subtitle!) + : _Title(title: title), + body: child, + ), + ), + ); + } +} + +class _Title extends StatelessWidget { + const _Title({Key? key, required this.title}) : super(key: key); + + final String title; + + @override + Widget build(BuildContext context) => Text( + title, + style: Theme.of(context).textTheme.headline3!.copyWith( + fontWeight: FontWeight.bold, + color: PinballColors.darkBlue, + ), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ); +} + +class _TitleAndSubtitle extends StatelessWidget { + const _TitleAndSubtitle({ + Key? key, + required this.title, + required this.subtitle, + }) : super(key: key); + + final String title; + final String subtitle; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _Title(title: title), + Text( + subtitle, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: textTheme.headline3!.copyWith(fontWeight: FontWeight.normal), + ), + ], + ); + } +} diff --git a/packages/pinball_ui/lib/src/theme/pinball_text_style.dart b/packages/pinball_ui/lib/src/theme/pinball_text_style.dart index 378078fa..5e0a7fa2 100644 --- a/packages/pinball_ui/lib/src/theme/pinball_text_style.dart +++ b/packages/pinball_ui/lib/src/theme/pinball_text_style.dart @@ -19,10 +19,11 @@ abstract class PinballTextStyle { fontSize: 24, package: _fontPackage, fontFamily: _primaryFontFamily, + color: PinballColors.white, ); static const headline3 = TextStyle( - color: PinballColors.white, + color: PinballColors.darkBlue, fontSize: 20, package: _fontPackage, fontFamily: _primaryFontFamily, diff --git a/packages/pinball_ui/lib/src/widgets/pinball_button.dart b/packages/pinball_ui/lib/src/widgets/pinball_button.dart new file mode 100644 index 00000000..585a8d54 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/pinball_button.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:pinball_ui/gen/gen.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +/// {@template pinball_button} +/// Pinball-themed button with pixel art. +/// {@endtemplate} +class PinballButton extends StatelessWidget { + /// {@macro pinball_button} + const PinballButton({ + Key? key, + required this.text, + required this.onTap, + }) : super(key: key); + + /// Text of the button. + final String text; + + /// Tap callback. + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(Assets.images.button.pinballButton.keyName), + ), + ), + child: Center( + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: Text( + text, + style: Theme.of(context) + .textTheme + .headline3! + .copyWith(color: PinballColors.white), + ), + ), + ), + ), + ); + } +} diff --git a/packages/pinball_ui/lib/src/widgets/widgets.dart b/packages/pinball_ui/lib/src/widgets/widgets.dart new file mode 100644 index 00000000..34d952b6 --- /dev/null +++ b/packages/pinball_ui/lib/src/widgets/widgets.dart @@ -0,0 +1 @@ +export 'pinball_button.dart'; diff --git a/packages/pinball_ui/pubspec.yaml b/packages/pinball_ui/pubspec.yaml index a89f7a67..747b1b8f 100644 --- a/packages/pinball_ui/pubspec.yaml +++ b/packages/pinball_ui/pubspec.yaml @@ -23,6 +23,7 @@ flutter: generate: true assets: - assets/images/dialog/ + - assets/images/button/ fonts: - family: PixeloidSans fonts: diff --git a/packages/pinball_ui/test/src/dialog/pinball_dialog_test.dart b/packages/pinball_ui/test/src/dialog/pinball_dialog_test.dart new file mode 100644 index 00000000..85e2c4da --- /dev/null +++ b/packages/pinball_ui/test/src/dialog/pinball_dialog_test.dart @@ -0,0 +1,44 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballDialog', () { + group('with title only', () { + testWidgets('renders the title and the body', (tester) async { + tester.binding.window.physicalSizeTestValue = const Size(2000, 4000); + addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + await tester.pumpWidget( + const MaterialApp( + home: PinballDialog(title: 'title', child: Placeholder()), + ), + ); + expect(find.byType(PixelatedDecoration), findsOneWidget); + expect(find.text('title'), findsOneWidget); + expect(find.byType(Placeholder), findsOneWidget); + }); + }); + + group('with title and subtitle', () { + testWidgets('renders the title, subtitle and the body', (tester) async { + tester.binding.window.physicalSizeTestValue = const Size(2000, 4000); + addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + await tester.pumpWidget( + MaterialApp( + home: PinballDialog( + title: 'title', + subtitle: 'sub', + child: Icon(Icons.home), + ), + ), + ); + expect(find.byType(PixelatedDecoration), findsOneWidget); + expect(find.text('title'), findsOneWidget); + expect(find.text('sub'), findsOneWidget); + expect(find.byType(Icon), findsOneWidget); + }); + }); + }); +} diff --git a/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart b/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart index 60b382f3..2af092b2 100644 --- a/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart +++ b/packages/pinball_ui/test/src/theme/pinball_text_style_test.dart @@ -14,10 +14,10 @@ void main() { expect(style.fontSize, 24); }); - test('headline3 has fontSize 20 and white color', () { + test('headline3 has fontSize 20 and dark blue color', () { const style = PinballTextStyle.headline3; expect(style.fontSize, 20); - expect(style.color, PinballColors.white); + expect(style.color, PinballColors.darkBlue); }); test('headline4 has fontSize 16 and white color', () { diff --git a/packages/pinball_ui/test/src/widgets/pinball_button_test.dart b/packages/pinball_ui/test/src/widgets/pinball_button_test.dart new file mode 100644 index 00000000..064fbb6a --- /dev/null +++ b/packages/pinball_ui/test/src/widgets/pinball_button_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_ui/pinball_ui.dart'; + +void main() { + group('PinballButton', () { + testWidgets('renders the given text and responds to taps', (tester) async { + var wasTapped = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PinballButton( + text: 'test', + onTap: () { + wasTapped = true; + }, + ), + ), + ), + ), + ); + await tester.tap(find.text('test')); + expect(wasTapped, isTrue); + }); + }); +} diff --git a/test/game/view/widgets/play_button_overlay_test.dart b/test/game/view/widgets/play_button_overlay_test.dart index 0345978d..2229f4b5 100644 --- a/test/game/view/widgets/play_button_overlay_test.dart +++ b/test/game/view/widgets/play_button_overlay_test.dart @@ -1,3 +1,4 @@ +import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; @@ -9,37 +10,46 @@ void main() { group('PlayButtonOverlay', () { late PinballGame game; late GameFlowController gameFlowController; + late CharacterThemeCubit characterThemeCubit; setUp(() { game = MockPinballGame(); gameFlowController = MockGameFlowController(); - + characterThemeCubit = MockCharacterThemeCubit(); + whenListen( + characterThemeCubit, + const Stream.empty(), + initialState: const CharacterThemeState.initial(), + ); + when(() => characterThemeCubit.state) + .thenReturn(const CharacterThemeState.initial()); when(() => game.gameFlowController).thenReturn(gameFlowController); when(gameFlowController.start).thenAnswer((_) {}); }); testWidgets('renders correctly', (tester) async { await tester.pumpApp(PlayButtonOverlay(game: game)); - expect(find.text('Play'), findsOneWidget); }); - testWidgets('calls gameFlowController.start when taped', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); - + testWidgets('calls gameFlowController.start when tapped', (tester) async { + await tester.pumpApp( + PlayButtonOverlay(game: game), + characterThemeCubit: characterThemeCubit, + ); await tester.tap(find.text('Play')); await tester.pump(); - verify(gameFlowController.start).called(1); }); testWidgets('displays CharacterSelectionDialog when tapped', (tester) async { - await tester.pumpApp(PlayButtonOverlay(game: game)); - + await tester.pumpApp( + PlayButtonOverlay(game: game), + characterThemeCubit: characterThemeCubit, + ); await tester.tap(find.text('Play')); - await tester.pump(); - + await tester.pumpAndSettle(); expect(find.byType(CharacterSelectionDialog), findsOneWidget); }); }); diff --git a/test/select_character/view/character_selection_page_test.dart b/test/select_character/view/character_selection_page_test.dart index dc5d70ea..9a94fdbd 100644 --- a/test/select_character/view/character_selection_page_test.dart +++ b/test/select_character/view/character_selection_page_test.dart @@ -1,14 +1,11 @@ -// ignore_for_file: prefer_const_constructors - -import 'dart:async'; - import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/select_character/select_character.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_theme/pinball_theme.dart'; +import 'package:pinball_ui/pinball_ui.dart'; import '../../helpers/helpers.dart'; @@ -22,141 +19,54 @@ void main() { const Stream.empty(), initialState: const CharacterThemeState.initial(), ); + when(() => characterThemeCubit.state) + .thenReturn(const CharacterThemeState.initial()); }); - group('CharacterSelectionPage', () { - testWidgets('renders CharacterSelectionView', (tester) async { - await tester.pumpApp( - CharacterSelectionDialog(), - characterThemeCubit: characterThemeCubit, - ); - expect(find.byType(CharacterSelectionView), findsOneWidget); - }); - - testWidgets('route returns a valid navigation route', (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( + group('CharacterSelectionDialog', () { + group('showCharacterSelectionDialog', () { + testWidgets('inflates the dialog', (tester) async { + await tester.pumpApp( + Builder( builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context) - .push(CharacterSelectionDialog.route()); - }, - child: Text('Tap me'), + return TextButton( + onPressed: () => showCharacterSelectionDialog(context), + child: const Text('test'), ); }, ), - ), - characterThemeCubit: characterThemeCubit, - ); - - await tester.tap(find.text('Tap me')); - await tester.pumpAndSettle(); - - expect(find.byType(CharacterSelectionDialog), findsOneWidget); + characterThemeCubit: characterThemeCubit, + ); + await tester.tap(find.text('test')); + await tester.pumpAndSettle(); + expect(find.byType(CharacterSelectionDialog), findsOneWidget); + }); }); - }); - group('CharacterSelectionView', () { - testWidgets('renders correctly', (tester) async { - const titleText = 'Choose your character!'; + testWidgets('selecting a new character calls characterSelected on cubit', + (tester) async { await tester.pumpApp( - CharacterSelectionView(), + const CharacterSelectionDialog(), characterThemeCubit: characterThemeCubit, ); - - expect(find.text(titleText), findsOneWidget); - expect(find.byType(CharacterImageButton), findsNWidgets(4)); - expect(find.byType(TextButton), findsOneWidget); + await tester.tap(find.byKey(const Key('sparky_character_selection'))); + await tester.pumpAndSettle(); + verify( + () => characterThemeCubit.characterSelected(const SparkyTheme()), + ).called(1); }); - testWidgets('calls characterSelected when a character image is tapped', - (tester) async { - const sparkyButtonKey = Key('characterSelectionPage_sparkyButton'); - + testWidgets( + 'tapping the select button dismisses the character ' + 'dialog and shows the how to play dialog', (tester) async { await tester.pumpApp( - CharacterSelectionView(), + const CharacterSelectionDialog(), characterThemeCubit: characterThemeCubit, ); - - await tester.tap(find.byKey(sparkyButtonKey)); - - verify(() => characterThemeCubit.characterSelected(SparkyTheme())) - .called(1); - }); - - group('HowToPlayDialog', () { - testWidgets( - 'is displayed for 3 seconds when start is tapped', - (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context) - .push(CharacterSelectionDialog.route()); - }, - child: Text('Tap me'), - ); - }, - ), - ), - characterThemeCubit: characterThemeCubit, - ); - await tester.tap(find.text('Tap me')); - await tester.pumpAndSettle(); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsOneWidget); - await tester.pump(Duration(seconds: 3)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsNothing); - }, - ); - - testWidgets( - 'can be dismissed manually before 3 seconds have passed', - (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (context) { - return ElevatedButton( - onPressed: () { - Navigator.of(context) - .push(CharacterSelectionDialog.route()); - }, - child: Text('Tap me'), - ); - }, - ), - ), - characterThemeCubit: characterThemeCubit, - ); - await tester.tap(find.text('Tap me')); - await tester.pumpAndSettle(); - await tester.ensureVisible(find.byType(TextButton)); - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsOneWidget); - await tester.tapAt(Offset(1, 1)); - await tester.pumpAndSettle(); - expect(find.byType(HowToPlayDialog), findsNothing); - }, - ); + await tester.tap(find.byType(PinballButton)); + await tester.pumpAndSettle(); + expect(find.byType(CharacterSelectionDialog), findsNothing); + expect(find.byType(HowToPlayDialog), findsOneWidget); }); }); - - testWidgets('CharacterImageButton renders correctly', (tester) async { - await tester.pumpApp( - CharacterImageButton(DashTheme()), - characterThemeCubit: characterThemeCubit, - ); - - expect(find.byType(Image), findsOneWidget); - }); } diff --git a/test/start_game/widgets/how_to_play_dialog_test.dart b/test/start_game/widgets/how_to_play_dialog_test.dart index 1de4c2ad..cf8b4971 100644 --- a/test/start_game/widgets/how_to_play_dialog_test.dart +++ b/test/start_game/widgets/how_to_play_dialog_test.dart @@ -55,14 +55,31 @@ void main() { expect(find.text(l10n.tapAndHoldRocket), findsOneWidget); expect(find.text(l10n.tapLeftRightScreen), findsOneWidget); }); - }); - group('KeyButton', () { - testWidgets('renders correctly', (tester) async { + testWidgets('disappears after 3 seconds', (tester) async { await tester.pumpApp( - KeyButton(control: Control.a), + Builder( + builder: (context) { + return TextButton( + onPressed: () => showHowToPlayDialog(context), + child: const Text('test'), + ); + }, + ), ); + expect(find.byType(HowToPlayDialog), findsNothing); + await tester.tap(find.text('test')); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsOneWidget); + await tester.pump(const Duration(seconds: 4)); + await tester.pumpAndSettle(); + expect(find.byType(HowToPlayDialog), findsNothing); + }); + }); + group('KeyButton', () { + testWidgets('renders correctly', (tester) async { + await tester.pumpApp(KeyButton(control: Control.a)); expect(find.text('A'), findsOneWidget); }); });