diff --git a/lib/game/components/flutter_forest.dart b/lib/game/components/flutter_forest.dart index ad6cfea1..20628d35 100644 --- a/lib/game/components/flutter_forest.dart +++ b/lib/game/components/flutter_forest.dart @@ -16,11 +16,10 @@ import 'package:pinball_components/pinball_components.dart'; /// is awarded, and the [BigDashNestBumper] releases a new [Ball]. /// {@endtemplate} // TODO(alestiago): Make a [Blueprint] once [Blueprint] inherits from [Component]. -class FlutterForest extends Component - with Controls, HasGameRef { +class FlutterForest extends Component with Controls<_FlutterForestController> { /// {@macro flutter_forest} FlutterForest() { - controller = FlutterForestController(this); + controller = _FlutterForestController(this); } @override @@ -28,14 +27,15 @@ class FlutterForest extends Component await super.onLoad(); final signPost = FlutterSignPost()..initialPosition = Vector2(8.35, 58.3); - final bigNest = ControlledBigDashNestBumper(id: 'big_nest_bumper') - ..initialPosition = Vector2(18.55, 59.35); - final smallLeftNest = - ControlledSmallDashNestBumper.a(id: 'small_nest_bumper_a') - ..initialPosition = Vector2(8.95, 51.95); - final smallRightNest = - ControlledSmallDashNestBumper.b(id: 'small_nest_bumper_b') - ..initialPosition = Vector2(23.3, 46.75); + final bigNest = _ControlledBigDashNestBumper( + id: 'big_nest_bumper', + )..initialPosition = Vector2(18.55, 59.35); + final smallLeftNest = _ControlledSmallDashNestBumper.a( + id: 'small_nest_bumper_a', + )..initialPosition = Vector2(8.95, 51.95); + final smallRightNest = _ControlledSmallDashNestBumper.b( + id: 'small_nest_bumper_b', + )..initialPosition = Vector2(23.3, 46.75); await addAll([ signPost, @@ -49,15 +49,15 @@ class FlutterForest extends Component /// {@template flutter_forest_controller} /// /// {@endtemplate} -class FlutterForestController extends ComponentController +class _FlutterForestController extends ComponentController with BlocComponent, HasGameRef { /// {@macro flutter_forest_controller} - FlutterForestController(FlutterForest flutterForest) : super(flutterForest); + _FlutterForestController(FlutterForest flutterForest) : super(flutterForest); @override Future onLoad() async { await super.onLoad(); - gameRef.addContactCallback(ControlledDashNestBumperBallContactCallback()); + gameRef.addContactCallback(_ControlledDashNestBumperBallContactCallback()); } @override @@ -81,10 +81,10 @@ class FlutterForestController extends ComponentController /// {@template controlled_big_dash_nest_bumper} /// A [BigDashNestBumper] controlled by a [DashNestBumperController]. /// {@endtemplate} -class ControlledBigDashNestBumper extends BigDashNestBumper +class _ControlledBigDashNestBumper extends BigDashNestBumper with Controls, ScorePoints { /// {@macro controlled_big_dash_nest_bumper} - ControlledBigDashNestBumper({required String id}) : super() { + _ControlledBigDashNestBumper({required String id}) : super() { controller = DashNestBumperController(this, id: id); } @@ -95,15 +95,15 @@ class ControlledBigDashNestBumper extends BigDashNestBumper /// {@template controlled_small_dash_nest_bumper} /// A [SmallDashNestBumper] controlled by a [DashNestBumperController]. /// {@endtemplate} -class ControlledSmallDashNestBumper extends SmallDashNestBumper +class _ControlledSmallDashNestBumper extends SmallDashNestBumper with Controls, ScorePoints { /// {@macro controlled_small_dash_nest_bumper} - ControlledSmallDashNestBumper.a({required String id}) : super.a() { + _ControlledSmallDashNestBumper.a({required String id}) : super.a() { controller = DashNestBumperController(this, id: id); } /// {@macro controlled_small_dash_nest_bumper} - ControlledSmallDashNestBumper.b({required String id}) : super.b() { + _ControlledSmallDashNestBumper.b({required String id}) : super.b() { controller = DashNestBumperController(this, id: id); } @@ -114,24 +114,24 @@ class ControlledSmallDashNestBumper extends SmallDashNestBumper /// {@template dash_nest_bumper_controller} /// Controls a [DashNestBumper]. /// {@endtemplate} +@visibleForTesting class DashNestBumperController extends ComponentController with BlocComponent, HasGameRef { /// {@macro dash_nest_bumper_controller} DashNestBumperController( DashNestBumper dashNestBumper, { - required String id, - }) : _id = id, - super(dashNestBumper); + required this.id, + }) : super(dashNestBumper); /// Unique identifier for the controlled [DashNestBumper]. /// /// Used to identify [DashNestBumper]s in [GameState.activatedDashNests]. - final String _id; + final String id; @override bool listenWhen(GameState? previousState, GameState newState) { - final wasActive = previousState?.activatedDashNests.contains(_id) ?? false; - final isActive = newState.activatedDashNests.contains(_id); + final wasActive = previousState?.activatedDashNests.contains(id) ?? false; + final isActive = newState.activatedDashNests.contains(id); return wasActive != isActive; } @@ -140,7 +140,7 @@ class DashNestBumperController extends ComponentController void onNewState(GameState state) { super.onNewState(state); - if (state.activatedDashNests.contains(_id)) { + if (state.activatedDashNests.contains(id)) { component.activate(); } else { component.deactivate(); @@ -149,15 +149,14 @@ class DashNestBumperController extends ComponentController /// Registers when a [DashNestBumper] is hit by a [Ball]. /// - /// Triggered by [ControlledDashNestBumperBallContactCallback]. + /// Triggered by [_ControlledDashNestBumperBallContactCallback]. void hit() { - gameRef.read().add(DashNestActivated(_id)); + gameRef.read().add(DashNestActivated(id)); } } /// Listens when a [Ball] bounces bounces against a [DashNestBumper] -@visibleForTesting -class ControlledDashNestBumperBallContactCallback +class _ControlledDashNestBumperBallContactCallback extends ContactCallback, Ball> { @override void begin( diff --git a/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart b/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart new file mode 100644 index 00000000..6aa8f566 --- /dev/null +++ b/packages/pinball_components/test/src/components/dash_nest_bumper_test.dart @@ -0,0 +1,34 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(TestGame.new); + group('BigDashNestBumper', () { + flameTester.test('loads correctly', (game) async { + final bigNest = BigDashNestBumper(); + await game.ensureAdd(bigNest); + expect(game.contains(bigNest), isTrue); + }); + }); + + group('SmallDashNestBumper', () { + flameTester.test('"a" loads correctly', (game) async { + final smallNest = SmallDashNestBumper.a(); + await game.ensureAdd(smallNest); + + expect(game.contains(smallNest), isTrue); + }); + + flameTester.test('"b" loads correctly', (game) async { + final smallNest = SmallDashNestBumper.b(); + await game.ensureAdd(smallNest); + expect(game.contains(smallNest), isTrue); + }); + }); +} diff --git a/test/game/components/flutter_forest_test.dart b/test/game/components/flutter_forest_test.dart index 6c36090d..b6a5311b 100644 --- a/test/game/components/flutter_forest_test.dart +++ b/test/game/components/flutter_forest_test.dart @@ -1,7 +1,12 @@ // ignore_for_file: cascade_invocations +import 'dart:math'; + import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; @@ -9,6 +14,18 @@ import 'package:pinball_components/pinball_components.dart'; import '../../helpers/helpers.dart'; +void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) { + assert( + bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty, + 'Bodies require fixtures to contact each other.', + ); + + final fixtureA = bodyA.body.fixtures.first; + final fixtureB = bodyB.body.fixtures.first; + final contact = Contact.init(fixtureA, 0, fixtureB, 0); + game.world.contactManager.contactListener?.beginContact(contact); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGameTest.create); @@ -30,13 +47,73 @@ void main() { 'a FlutterSignPost', (game) async { await game.ready(); + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.descendants().whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'a BigDashNestBumper', + (game) async { + await game.ready(); + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); expect( - game.descendants().whereType().length, + flutterForest.descendants().whereType().length, equals(1), ); }, ); + + flameTester.test( + 'two SmallDashNestBumper', + (game) async { + await game.ready(); + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + + expect( + flutterForest.descendants().whereType().length, + equals(2), + ); + }, + ); + }); + + group('controller', () { + group('listenWhen', () { + final gameBloc = MockGameBloc(); + final tester = flameBlocTester( + game: TestGame.new, + gameBloc: () => gameBloc, + ); + + tester.testGameWidget( + 'listens when a Bonus.dashNest is added', + verify: (game, tester) async { + final flutterForest = FlutterForest(); + + const state = GameState( + score: 0, + balls: 3, + activatedBonusLetters: [], + activatedDashNests: {}, + bonusHistory: [GameBonus.dashNest], + ); + expect( + flutterForest.controller + .listenWhen(const GameState.initial(), state), + isTrue, + ); + }, + ); + }); }); flameTester.test( @@ -47,7 +124,7 @@ void main() { await game.ensureAdd(flutterForest); final previousBalls = game.descendants().whereType().length; - flutterForest.onNewState(MockGameState()); + flutterForest.controller.onNewState(MockGameState()); await game.ready(); expect( @@ -57,14 +134,13 @@ void main() { }, ); - group('listenWhen', () { - final gameBloc = MockGameBloc(); - final tester = flameBlocTester( - game: TestGame.new, - gameBloc: () => gameBloc, - ); + group('bumpers', () { + late Ball ball; + late GameBloc gameBloc; setUp(() { + ball = Ball(baseColor: const Color(0xFF00FFFF)); + gameBloc = MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -72,80 +148,170 @@ void main() { ); }); + final tester = flameBlocTester( + game: PinballGameTest.create, + gameBloc: () => gameBloc, + ); + tester.testGameWidget( - 'listens when a Bonus.dashNest is added', - verify: (game, tester) async { + 'add DashNestActivated event', + setUp: (game, tester) async { final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + await game.ensureAdd(ball); - const state = GameState( - score: 0, - balls: 3, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [GameBonus.dashNest], - ); - expect( - flutterForest.listenWhen(const GameState.initial(), state), - isTrue, - ); + final bumpers = + flutterForest.descendants().whereType(); + + for (final bumper in bumpers) { + beginContact(game, bumper, ball); + final controller = bumper.firstChild()!; + // TODO(alestiago): Investiagate why is is being called twice + // instead of once. + verify( + () => gameBloc.add(DashNestActivated(controller.id)), + ).called(2); + } + }, + ); + + tester.testGameWidget( + 'add Scored event', + setUp: (game, tester) async { + final flutterForest = FlutterForest(); + await game.ensureAdd(flutterForest); + await game.ensureAdd(ball); + + final bumpers = + flutterForest.descendants().whereType(); + + for (final bumper in bumpers) { + beginContact(game, bumper, ball); + final points = (bumper as ScorePoints).points; + verify( + () => gameBloc.add(Scored(points: points)), + ).called(1); + } }, ); }); }); - group('DashNestBumperBallContactCallback', () { - final gameBloc = MockGameBloc(); - final tester = flameBlocTester( - // TODO(alestiago): Use TestGame.new once a controller is implemented. - game: PinballGameTest.create, - gameBloc: () => gameBloc, - ); + group('DashNestBumperController', () { + late DashNestBumper dashNestBumper; setUp(() { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); + dashNestBumper = MockDashNestBumper(); }); - final dashNestBumper = MockDashNestBumper(); - tester.testGameWidget( - 'adds a DashNestActivated event with DashNestBumper.id', - setUp: (game, tester) async { - const id = '0'; - when(() => dashNestBumper.id).thenReturn(id); - when(() => dashNestBumper.gameRef).thenReturn(game); - }, - verify: (game, tester) async { - final contactCallback = DashNestBumperBallContactCallback(); - contactCallback.begin(dashNestBumper, MockBall(), MockContact()); + group( + 'listensWhen', + () { + late GameState previousState; + late GameState newState; + + setUp( + () { + previousState = MockGameState(); + newState = MockGameState(); + }, + ); + + test('listens when the id is added to activatedDashNests', () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => previousState.activatedDashNests).thenReturn({}); + when(() => newState.activatedDashNests).thenReturn({id}); + + expect(controller.listenWhen(previousState, newState), isTrue); + }); + + test('listens when the id is removed to activatedDashNests', () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => previousState.activatedDashNests).thenReturn({id}); + when(() => newState.activatedDashNests).thenReturn({}); + + expect(controller.listenWhen(previousState, newState), isTrue); + }); + + test("doesn't listen when the id is never in activatedDashNests", () { + final controller = DashNestBumperController( + dashNestBumper, + id: '', + ); - verify( - () => gameBloc.add(DashNestActivated(dashNestBumper.id)), - ).called(1); + when(() => previousState.activatedDashNests).thenReturn({}); + when(() => newState.activatedDashNests).thenReturn({}); + + expect(controller.listenWhen(previousState, newState), isFalse); + }); + + test("doesn't listen when the id still in activatedDashNests", () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => previousState.activatedDashNests).thenReturn({id}); + when(() => newState.activatedDashNests).thenReturn({id}); + + expect(controller.listenWhen(previousState, newState), isFalse); + }); }, ); - }); - group('BigDashNestBumper', () { - test('has points', () { - final dashNestBumper = BigDashNestBumper(id: ''); - expect(dashNestBumper.points, greaterThan(0)); - }); - }); + group( + 'onNewState', + () { + late GameState state; - group('SmallDashNestBumper', () { - group('has points', () { - test('when a', () { - final dashNestBumper = SmallDashNestBumper.a(id: ''); - expect(dashNestBumper.points, greaterThan(0)); - }); + setUp(() { + state = MockGameState(); + }); - test('when b', () { - final dashNestBumper = SmallDashNestBumper.b(id: ''); - expect(dashNestBumper.points, greaterThan(0)); - }); - }); + test( + 'activates the bumper when id in activatedDashNests', + () { + const id = ''; + final controller = DashNestBumperController( + dashNestBumper, + id: id, + ); + + when(() => state.activatedDashNests).thenReturn({id}); + controller.onNewState(state); + + verify(() => dashNestBumper.activate()).called(1); + }, + ); + + test( + 'deactivates the bumper when id not in activatedDashNests', + () { + final controller = DashNestBumperController( + dashNestBumper, + id: '', + ); + + when(() => state.activatedDashNests).thenReturn({}); + controller.onNewState(state); + + verify(() => dashNestBumper.deactivate()).called(1); + }, + ); + }, + ); }); } + +class MockDashNestBumper extends Mock implements DashNestBumper {}