diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 7e3fdf17..36e9e842 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -6,29 +6,34 @@ // https://opensource.org/licenses/MIT. import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/landing/landing.dart'; +import 'package:pinball/theme/theme.dart'; class App extends StatelessWidget { const App({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return MaterialApp( - title: 'I/O Pinball', - theme: ThemeData( - appBarTheme: const AppBarTheme(color: Color(0xFF13B9FF)), - colorScheme: ColorScheme.fromSwatch( - accentColor: const Color(0xFF13B9FF), + return BlocProvider( + create: (_) => ThemeCubit(), + child: MaterialApp( + title: 'I/O Pinball', + theme: ThemeData( + appBarTheme: const AppBarTheme(color: Color(0xFF13B9FF)), + colorScheme: ColorScheme.fromSwatch( + accentColor: const Color(0xFF13B9FF), + ), ), + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: const LandingPage(), ), - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - ], - supportedLocales: AppLocalizations.supportedLocales, - home: const LandingPage(), ); } } diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index e285b14b..f7904ff6 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -3,20 +3,21 @@ import 'package:flame_forge2d/body_component.dart'; import 'package:flutter/material.dart'; import 'package:forge2d/forge2d.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/theme/theme.dart'; class Ball extends BodyComponent - with BlocComponent { + with BlocComponent { Ball({ required Vector2 position, - }) : _position = position { - // TODO(alestiago): Use asset instead of color when provided. - paint = Paint()..color = const Color(0xFFFFFFFF); - } + }) : _position = position; final Vector2 _position; @override Body createBody() { + paint = Paint() + ..color = gameRef.read().state.theme.characterTheme.ballColor; + final shape = CircleShape()..radius = 2; final fixtureDef = FixtureDef(shape)..density = 1; diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index fa732ac7..cce082f1 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,4 +1,15 @@ { "@@locale": "en", - "play": "Play" + "play": "Play", + "@play": { + "description": "Text displayed on the landing page play button" + }, + "start": "Start", + "@start": { + "description": "Text displayed on the character seleciton page start button" + }, + "characterSelectionTitle": "Choose your character!", + "@characterSelectionTitle": { + "description": "Title text displayed on the character seleciton page" + } } \ No newline at end of file diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 405ef51f..749f08b5 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -1,4 +1,15 @@ { "@@locale": "es", - "play": "Jugar" + "play": "Jugar", + "@play": { + "description": "Text displayed on the landing page play button" + }, + "start": "Comienzo", + "@start": { + "description": "Text displayed on the character seleciton page start button" + }, + "characterSelectionTitle": "¡Elige a tu personaje!", + "@characterSelectionTitle": { + "description": "Title text displayed on the character seleciton page" + } } \ No newline at end of file diff --git a/lib/landing/view/landing_page.dart b/lib/landing/view/landing_page.dart index a688dee1..56bd53bc 100644 --- a/lib/landing/view/landing_page.dart +++ b/lib/landing/view/landing_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; class LandingPage extends StatelessWidget { const LandingPage({Key? key}) : super(key: key); @@ -8,11 +8,12 @@ class LandingPage extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + return Scaffold( body: Center( child: TextButton( onPressed: () => - Navigator.of(context).push(PinballGamePage.route()), + Navigator.of(context).push(CharacterSelectionPage.route()), child: Text(l10n.play), ), ), diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index fcf5d9ee..f6318400 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1 +1,2 @@ export 'cubit/theme_cubit.dart'; +export 'view/view.dart'; diff --git a/lib/theme/view/character_selection_page.dart b/lib/theme/view/character_selection_page.dart new file mode 100644 index 00000000..15bdb81b --- /dev/null +++ b/lib/theme/view/character_selection_page.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; +import 'package:pinball_theme/pinball_theme.dart'; +import 'package:provider/provider.dart'; + +class CharacterSelectionPage extends StatelessWidget { + const CharacterSelectionPage({Key? key}) : super(key: key); + + static Route route() { + return MaterialPageRoute( + builder: (_) => const CharacterSelectionPage(), + ); + } + + @override + Widget build(BuildContext context) { + return const CharacterSelectionView(); + } +} + +class CharacterSelectionView extends StatelessWidget { + const CharacterSelectionView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Scaffold( + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 80), + Text( + l10n.characterSelectionTitle, + style: Theme.of(context).textTheme.headline3, + ), + const SizedBox(height: 80), + const _CharacterSelectionGridView(), + const SizedBox(height: 20), + TextButton( + onPressed: () => + Navigator.of(context).push(PinballGamePage.route()), + child: Text(l10n.start), + ), + ], + ), + ), + ); + } +} + +class _CharacterSelectionGridView extends StatelessWidget { + const _CharacterSelectionGridView({Key? key}) : super(key: key); + + @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'), + ), + ], + ), + ); + } +} + +class _CharacterImageButton extends StatelessWidget { + const _CharacterImageButton( + this.characterTheme, { + Key? key, + }) : super(key: key); + + final CharacterTheme characterTheme; + + @override + Widget build(BuildContext context) { + final currentCharacterTheme = + context.select((ThemeCubit cubit) => cubit.state.theme.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.characterAsset.image(), + ), + ), + ); + } +} diff --git a/lib/theme/view/view.dart b/lib/theme/view/view.dart new file mode 100644 index 00000000..1af489b5 --- /dev/null +++ b/lib/theme/view/view.dart @@ -0,0 +1 @@ +export 'character_selection_page.dart'; diff --git a/packages/pinball_theme/analysis_options.yaml b/packages/pinball_theme/analysis_options.yaml index 3742fc3d..5e587410 100644 --- a/packages/pinball_theme/analysis_options.yaml +++ b/packages/pinball_theme/analysis_options.yaml @@ -1 +1,4 @@ -include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file +include: package:very_good_analysis/analysis_options.2.4.0.yaml +analyzer: + exclude: + - lib/**/*.gen.dart \ No newline at end of file diff --git a/packages/pinball_theme/assets/images/android.png b/packages/pinball_theme/assets/images/android.png new file mode 100644 index 00000000..23f677a5 Binary files /dev/null and b/packages/pinball_theme/assets/images/android.png differ diff --git a/packages/pinball_theme/assets/images/dash.png b/packages/pinball_theme/assets/images/dash.png new file mode 100644 index 00000000..43c074a3 Binary files /dev/null and b/packages/pinball_theme/assets/images/dash.png differ diff --git a/packages/pinball_theme/assets/images/dino.png b/packages/pinball_theme/assets/images/dino.png new file mode 100644 index 00000000..9e5dbf86 Binary files /dev/null and b/packages/pinball_theme/assets/images/dino.png differ diff --git a/packages/pinball_theme/assets/images/sparky.png b/packages/pinball_theme/assets/images/sparky.png new file mode 100644 index 00000000..8e484f26 Binary files /dev/null and b/packages/pinball_theme/assets/images/sparky.png differ diff --git a/packages/pinball_theme/lib/pinball_theme.dart b/packages/pinball_theme/lib/pinball_theme.dart index 0206fa7b..139a70dc 100644 --- a/packages/pinball_theme/lib/pinball_theme.dart +++ b/packages/pinball_theme/lib/pinball_theme.dart @@ -1,4 +1,5 @@ library pinball_theme; +export 'src/generated/generated.dart'; export 'src/pinball_theme.dart'; export 'src/themes/themes.dart'; diff --git a/packages/pinball_theme/lib/src/generated/assets.gen.dart b/packages/pinball_theme/lib/src/generated/assets.gen.dart new file mode 100644 index 00000000..9dc5c029 --- /dev/null +++ b/packages/pinball_theme/lib/src/generated/assets.gen.dart @@ -0,0 +1,71 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +import 'package:flutter/widgets.dart'; + +class $AssetsImagesGen { + const $AssetsImagesGen(); + + AssetGenImage get android => const AssetGenImage('assets/images/android.png'); + AssetGenImage get dash => const AssetGenImage('assets/images/dash.png'); + AssetGenImage get dino => const AssetGenImage('assets/images/dino.png'); + AssetGenImage get sparky => const AssetGenImage('assets/images/sparky.png'); +} + +class Assets { + Assets._(); + + static const $AssetsImagesGen images = $AssetsImagesGen(); +} + +class AssetGenImage extends AssetImage { + const AssetGenImage(String assetName) + : super(assetName, package: 'pinball_theme'); + + Image image({ + Key? key, + ImageFrameBuilder? frameBuilder, + ImageLoadingBuilder? loadingBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? width, + double? height, + Color? color, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + FilterQuality filterQuality = FilterQuality.low, + }) { + return Image( + key: key, + image: this, + frameBuilder: frameBuilder, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: width, + height: height, + color: color, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + ); + } + + String get path => assetName; +} diff --git a/packages/pinball_theme/lib/src/generated/generated.dart b/packages/pinball_theme/lib/src/generated/generated.dart new file mode 100644 index 00000000..e7ad4c54 --- /dev/null +++ b/packages/pinball_theme/lib/src/generated/generated.dart @@ -0,0 +1 @@ +export 'assets.gen.dart'; diff --git a/packages/pinball_theme/lib/src/themes/android_theme.dart b/packages/pinball_theme/lib/src/themes/android_theme.dart index 59c16bd9..f6605f52 100644 --- a/packages/pinball_theme/lib/src/themes/android_theme.dart +++ b/packages/pinball_theme/lib/src/themes/android_theme.dart @@ -10,4 +10,7 @@ class AndroidTheme extends CharacterTheme { @override Color get ballColor => Colors.green; + + @override + AssetGenImage get characterAsset => Assets.images.android; } diff --git a/packages/pinball_theme/lib/src/themes/character_theme.dart b/packages/pinball_theme/lib/src/themes/character_theme.dart index 8f81486a..9478f954 100644 --- a/packages/pinball_theme/lib/src/themes/character_theme.dart +++ b/packages/pinball_theme/lib/src/themes/character_theme.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:pinball_theme/pinball_theme.dart'; /// {@template character_theme} /// Base class for creating character themes. @@ -14,6 +15,9 @@ abstract class CharacterTheme extends Equatable { /// Ball color for this theme. Color get ballColor; + /// Asset for the theme character. + AssetGenImage get characterAsset; + @override List get props => [ballColor]; } diff --git a/packages/pinball_theme/lib/src/themes/dash_theme.dart b/packages/pinball_theme/lib/src/themes/dash_theme.dart index e4875a11..1b5b357e 100644 --- a/packages/pinball_theme/lib/src/themes/dash_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dash_theme.dart @@ -10,4 +10,7 @@ class DashTheme extends CharacterTheme { @override Color get ballColor => Colors.blue; + + @override + AssetGenImage get characterAsset => Assets.images.dash; } diff --git a/packages/pinball_theme/lib/src/themes/dino_theme.dart b/packages/pinball_theme/lib/src/themes/dino_theme.dart index 07776771..564cbea0 100644 --- a/packages/pinball_theme/lib/src/themes/dino_theme.dart +++ b/packages/pinball_theme/lib/src/themes/dino_theme.dart @@ -10,4 +10,7 @@ class DinoTheme extends CharacterTheme { @override Color get ballColor => Colors.grey; + + @override + AssetGenImage get characterAsset => Assets.images.dino; } diff --git a/packages/pinball_theme/lib/src/themes/sparky_theme.dart b/packages/pinball_theme/lib/src/themes/sparky_theme.dart index 5264bad6..b4181a8c 100644 --- a/packages/pinball_theme/lib/src/themes/sparky_theme.dart +++ b/packages/pinball_theme/lib/src/themes/sparky_theme.dart @@ -10,4 +10,7 @@ class SparkyTheme extends CharacterTheme { @override Color get ballColor => Colors.orange; + + @override + AssetGenImage get characterAsset => Assets.images.sparky; } diff --git a/packages/pinball_theme/pubspec.yaml b/packages/pinball_theme/pubspec.yaml index e9b3f215..7d745422 100644 --- a/packages/pinball_theme/pubspec.yaml +++ b/packages/pinball_theme/pubspec.yaml @@ -14,4 +14,16 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - very_good_analysis: ^2.4.0 \ No newline at end of file + very_good_analysis: ^2.4.0 + +flutter: + uses-material-design: true + generate: true + assets: + - assets/images/ + +flutter_gen: + assets: + package_parameter_enabled: true + output: lib/src/generated/ + line_length: 80 \ No newline at end of file diff --git a/packages/pinball_theme/test/src/themes/android_theme_test.dart b/packages/pinball_theme/test/src/themes/android_theme_test.dart index a6148042..24186c35 100644 --- a/packages/pinball_theme/test/src/themes/android_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/android_theme_test.dart @@ -17,5 +17,9 @@ void main() { test('ballColor is correct', () { expect(AndroidTheme().ballColor, equals(Colors.green)); }); + + test('characterAsset is correct', () { + expect(AndroidTheme().characterAsset, equals(Assets.images.android)); + }); }); } diff --git a/packages/pinball_theme/test/src/themes/dash_theme_test.dart b/packages/pinball_theme/test/src/themes/dash_theme_test.dart index 0d5c8293..2fb429e0 100644 --- a/packages/pinball_theme/test/src/themes/dash_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/dash_theme_test.dart @@ -17,5 +17,9 @@ void main() { test('ballColor is correct', () { expect(DashTheme().ballColor, equals(Colors.blue)); }); + + test('characterAsset is correct', () { + expect(DashTheme().characterAsset, equals(Assets.images.dash)); + }); }); } diff --git a/packages/pinball_theme/test/src/themes/dino_theme_test.dart b/packages/pinball_theme/test/src/themes/dino_theme_test.dart index 6efd8cbd..673cccf6 100644 --- a/packages/pinball_theme/test/src/themes/dino_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/dino_theme_test.dart @@ -17,5 +17,9 @@ void main() { test('ballColor is correct', () { expect(DinoTheme().ballColor, equals(Colors.grey)); }); + + test('characterAsset is correct', () { + expect(DinoTheme().characterAsset, equals(Assets.images.dino)); + }); }); } diff --git a/packages/pinball_theme/test/src/themes/sparky_theme_test.dart b/packages/pinball_theme/test/src/themes/sparky_theme_test.dart index 513ca219..d0d96566 100644 --- a/packages/pinball_theme/test/src/themes/sparky_theme_test.dart +++ b/packages/pinball_theme/test/src/themes/sparky_theme_test.dart @@ -17,5 +17,9 @@ void main() { test('ballColor is correct', () { expect(SparkyTheme().ballColor, equals(Colors.orange)); }); + + test('characterAsset is correct', () { + expect(SparkyTheme().characterAsset, equals(Assets.images.sparky)); + }); }); } diff --git a/pubspec.lock b/pubspec.lock index e218776d..7bf08da4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -324,6 +324,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + pinball_theme: + dependency: "direct main" + description: + path: "packages/pinball_theme" + relative: true + source: path + version: "1.0.0+1" pool: dependency: transitive description: diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index b32d16d5..14b77b73 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -6,6 +6,7 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/theme/cubit/theme_cubit.dart'; import '../../helpers/helpers.dart'; @@ -13,11 +14,30 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Ball', () { - final flameTester = FlameTester(PinballGame.new); + final gameBloc = MockGameBloc(); + final themeCubit = MockThemeCubit(); + + setUp(() { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + whenListen( + themeCubit, + const Stream.empty(), + initialState: const ThemeState.initial(), + ); + }); + + final tester = flameBlocTester( + gameBloc: gameBloc, + themeCubit: themeCubit, + ); - flameTester.test( + tester.widgetTest( 'loads correctly', - (game) async { + (game, tester) async { final ball = Ball(position: Vector2.zero()); await game.ensureAdd(ball); @@ -26,9 +46,9 @@ void main() { ); group('body', () { - flameTester.test( + tester.widgetTest( 'positions correctly', - (game) async { + (game, tester) async { final position = Vector2.all(10); final ball = Ball(position: position); await game.ensureAdd(ball); @@ -38,9 +58,9 @@ void main() { }, ); - flameTester.test( + tester.widgetTest( 'is dynamic', - (game) async { + (game, tester) async { final ball = Ball(position: Vector2.zero()); await game.ensureAdd(ball); @@ -50,9 +70,9 @@ void main() { }); group('first fixture', () { - flameTester.test( + tester.widgetTest( 'exists', - (game) async { + (game, tester) async { final ball = Ball(position: Vector2.zero()); await game.ensureAdd(ball); @@ -60,9 +80,9 @@ void main() { }, ); - flameTester.test( + tester.widgetTest( 'is dense', - (game) async { + (game, tester) async { final ball = Ball(position: Vector2.zero()); await game.ensureAdd(ball); @@ -71,9 +91,9 @@ void main() { }, ); - flameTester.test( + tester.widgetTest( 'shape is circular', - (game) async { + (game, tester) async { final ball = Ball(position: Vector2.zero()); await game.ensureAdd(ball); @@ -85,23 +105,6 @@ void main() { }); group('resetting a ball', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - }); - - final tester = flameBlocTester( - gameBlocBuilder: () { - return gameBloc; - }, - ); - tester.widgetTest( 'adds BallLost to GameBloc', (game, tester) async { diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 67e215fd..bf580f4e 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -5,6 +5,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/theme/theme.dart'; import '../../helpers/helpers.dart'; @@ -126,25 +127,30 @@ void main() { }); group('PlungerAnchorPrismaticJointDef', () { - late GameBloc gameBloc; late Plunger plunger; late Anchor anchor; + final gameBloc = MockGameBloc(); + final themeCubit = MockThemeCubit(); + setUp(() { - gameBloc = MockGameBloc(); + plunger = Plunger(position: Vector2.zero()); + anchor = Anchor(position: Vector2(0, -1)); whenListen( gameBloc, const Stream.empty(), initialState: const GameState.initial(), ); - plunger = Plunger(position: Vector2.zero()); - anchor = Anchor(position: Vector2(0, -1)); + whenListen( + themeCubit, + const Stream.empty(), + initialState: const ThemeState.initial(), + ); }); final flameTester = flameBlocTester( - gameBlocBuilder: () { - return gameBloc; - }, + gameBloc: gameBloc, + themeCubit: themeCubit, ); flameTester.test( diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index be418c1d..2481a59e 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -3,6 +3,7 @@ import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/theme/theme.dart'; import '../../helpers/helpers.dart'; @@ -21,6 +22,13 @@ void main() { }); testWidgets('route returns a valid navigation route', (tester) async { + final themeCubit = MockThemeCubit(); + whenListen( + themeCubit, + const Stream.empty(), + initialState: const ThemeState.initial(), + ); + await tester.pumpApp( Scaffold( body: Builder( @@ -34,6 +42,7 @@ void main() { }, ), ), + themeCubit: themeCubit, ); await tester.tap(find.text('Tap me')); @@ -67,14 +76,25 @@ void main() { 'renders a game over dialog when the user has lost', (tester) async { final gameBloc = MockGameBloc(); - const state = GameState(score: 0, balls: 0); + const gameState = GameState(score: 0, balls: 0); whenListen( gameBloc, - Stream.value(state), - initialState: state, + Stream.value(gameState), + initialState: gameState, ); - await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); + final themeCubit = MockThemeCubit(); + whenListen( + themeCubit, + const Stream.empty(), + initialState: const ThemeState.initial(), + ); + + await tester.pumpApp( + const PinballGameView(), + gameBloc: gameBloc, + themeCubit: themeCubit, + ); await tester.pump(); expect( diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart index e124052e..27cdad0d 100644 --- a/test/helpers/builders.dart +++ b/test/helpers/builders.dart @@ -1,16 +1,27 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/theme/theme.dart'; + +import 'mocks.dart'; FlameTester flameBlocTester({ - required GameBloc Function() gameBlocBuilder, + GameBloc? gameBloc, + ThemeCubit? themeCubit, }) { return FlameTester( PinballGame.new, pumpWidget: (gameWidget, tester) async { await tester.pumpWidget( - BlocProvider.value( - value: gameBlocBuilder(), + MultiBlocProvider( + providers: [ + BlocProvider.value( + value: gameBloc ?? MockGameBloc(), + ), + BlocProvider.value( + value: themeCubit ?? MockThemeCubit(), + ), + ], child: gameWidget, ), ); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index b46e2c5c..95ca9824 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -1,6 +1,7 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; +import 'package:pinball/theme/theme.dart'; class MockPinballGame extends Mock implements PinballGame {} @@ -13,3 +14,5 @@ class MockBall extends Mock implements Ball {} class MockContact extends Mock implements Contact {} class MockGameBloc extends Mock implements GameBloc {} + +class MockThemeCubit extends Mock implements ThemeCubit {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 2c1efd9f..e0b953d2 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -12,6 +12,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; +import 'package:pinball/theme/theme.dart'; import 'helpers.dart'; @@ -20,17 +21,25 @@ extension PumpApp on WidgetTester { Widget widget, { MockNavigator? navigator, GameBloc? gameBloc, + ThemeCubit? themeCubit, }) { return pumpWidget( - MaterialApp( - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, + MultiBlocProvider( + providers: [ + BlocProvider.value( + value: themeCubit ?? MockThemeCubit(), + ), + BlocProvider.value( + value: gameBloc ?? MockGameBloc(), + ), ], - supportedLocales: AppLocalizations.supportedLocales, - home: BlocProvider.value( - value: gameBloc ?? MockGameBloc(), - child: navigator != null + child: MaterialApp( + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: navigator != null ? MockNavigatorProvider(navigator: navigator, child: widget) : widget, ), diff --git a/test/landing/view/landing_page_test.dart b/test/landing/view/landing_page_test.dart index d754864c..ab036f9c 100644 --- a/test/landing/view/landing_page_test.dart +++ b/test/landing/view/landing_page_test.dart @@ -12,7 +12,7 @@ void main() { expect(find.byType(TextButton), findsOneWidget); }); - testWidgets('tapping on TextButton navigates to PinballGamePage', + testWidgets('tapping on TextButton navigates to CharacterSelectionPage', (tester) async { final navigator = MockNavigator(); when(() => navigator.push(any())).thenAnswer((_) async {}); diff --git a/test/theme/view/character_selection_page_test.dart b/test/theme/view/character_selection_page_test.dart new file mode 100644 index 00000000..956681a1 --- /dev/null +++ b/test/theme/view/character_selection_page_test.dart @@ -0,0 +1,101 @@ +// 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:mockingjay/mockingjay.dart'; +import 'package:pinball/theme/theme.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + late ThemeCubit themeCubit; + + setUp(() { + themeCubit = MockThemeCubit(); + whenListen( + themeCubit, + const Stream.empty(), + initialState: const ThemeState.initial(), + ); + }); + + group('CharacterSelectionPage', () { + testWidgets('renders CharacterSelectionView', (tester) async { + await tester.pumpApp( + const CharacterSelectionPage(), + themeCubit: themeCubit, + ); + expect(find.byType(CharacterSelectionView), findsOneWidget); + }); + + testWidgets('route returns a valid navigation route', (tester) async { + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + Navigator.of(context) + .push(CharacterSelectionPage.route()); + }, + child: const Text('Tap me'), + ); + }, + ), + ), + themeCubit: themeCubit, + ); + + await tester.tap(find.text('Tap me')); + await tester.pumpAndSettle(); + + expect(find.byType(CharacterSelectionPage), findsOneWidget); + }); + }); + + group('CharacterSelectionView', () { + testWidgets('renders correctly', (tester) async { + const titleText = 'Choose your character!'; + await tester.pumpApp( + CharacterSelectionView(), + themeCubit: themeCubit, + ); + + expect(find.text(titleText), findsOneWidget); + expect(find.byType(Image), findsNWidgets(4)); + expect(find.byType(TextButton), findsOneWidget); + }); + + testWidgets('calls characterSelected when a character image is tapped', + (tester) async { + const sparkyButtonKey = Key('characterSelectionPage_sparkyButton'); + + await tester.pumpApp( + CharacterSelectionView(), + themeCubit: themeCubit, + ); + + await tester.tap(find.byKey(sparkyButtonKey)); + + verify(() => themeCubit.characterSelected(SparkyTheme())).called(1); + }); + + testWidgets('navigates to PinballGamePage when start is tapped', + (tester) async { + final navigator = MockNavigator(); + when(() => navigator.push(any())).thenAnswer((_) async {}); + + await tester.pumpApp( + CharacterSelectionView(), + themeCubit: themeCubit, + navigator: navigator, + ); + await tester.ensureVisible(find.byType(TextButton)); + await tester.tap(find.byType(TextButton)); + + verify(() => navigator.push(any())).called(1); + }); + }); +}