diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index 3bba3b74..5bd4d92b 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -3,7 +3,7 @@ import 'package:pinball/game/game.dart'; /// {@template board} /// The main flat surface of the [PinballGame], where the [Flipper]s, -/// [RoundBumper]s, [SlingShot]s are arranged. +/// [RoundBumper]s, [Kicker]s are arranged. /// {entemplate} class Board extends Component { /// {@macro board} @@ -74,7 +74,7 @@ class _FlutterForest extends Component { /// {@template bottom_group} /// Grouping of the board's bottom [Component]s. /// -/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [SlingShot]s. +/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [Kicker]s. /// {@endtemplate} // TODO(alestiago): Consider renaming once entire Board is defined. class _BottomGroup extends Component { @@ -92,7 +92,7 @@ class _BottomGroup extends Component { @override Future onLoad() async { - final spacing = this.spacing + Flipper.width / 2; + final spacing = this.spacing + Flipper.size.x / 2; final rightSide = _BottomGroupSide( side: BoardSide.right, position: position + Vector2(spacing, 0), @@ -133,17 +133,17 @@ class _BottomGroupSide extends Component { final baseboard = Baseboard(side: _side) ..initialPosition = _position + Vector2( - (Flipper.width * direction) - direction, - Flipper.height, + (Flipper.size.x * direction) - direction, + Flipper.size.y, ); - final slingShot = SlingShot( + final kicker = Kicker( side: _side, )..initialPosition = _position + Vector2( - (Flipper.width) * direction, - Flipper.height + SlingShot.size.y, + (Flipper.size.x) * direction, + Flipper.size.y + Kicker.size.y, ); - await addAll([flipper, baseboard, slingShot]); + await addAll([flipper, baseboard, kicker]); } } diff --git a/lib/game/components/board_side.dart b/lib/game/components/board_side.dart index f7587f47..2ef8d651 100644 --- a/lib/game/components/board_side.dart +++ b/lib/game/components/board_side.dart @@ -3,7 +3,7 @@ import 'package:pinball/game/game.dart'; /// Indicates a side of the board. /// /// Usually used to position or mirror elements of a [PinballGame]; such as a -/// [Flipper] or [SlingShot]. +/// [Flipper] or [Kicker]. enum BoardSide { /// The left side of the board. left, diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 84525166..a255e652 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -7,6 +7,7 @@ export 'flipper.dart'; export 'initial_position.dart'; export 'jetpack_ramp.dart'; export 'joint_anchor.dart'; +export 'kicker.dart'; export 'launcher_ramp.dart'; export 'layer.dart'; export 'pathway.dart'; @@ -14,6 +15,5 @@ export 'plunger.dart'; export 'ramp_opening.dart'; export 'round_bumper.dart'; export 'score_points.dart'; -export 'sling_shot.dart'; export 'spaceship.dart'; export 'wall.dart'; diff --git a/lib/game/components/flipper.dart b/lib/game/components/flipper.dart index 77238acd..cf3fed4f 100644 --- a/lib/game/components/flipper.dart +++ b/lib/game/components/flipper.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flame/components.dart'; -import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -58,11 +57,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// Sprite is preloaded by [PinballGameAssetsX]. static const spritePath = 'components/flipper.png'; - /// The width of the [Flipper]. - static const width = 12.0; - - /// The height of the [Flipper]. - static const height = 2.8; + /// The size of the [Flipper]. + static final size = Vector2(12, 2.8); /// The speed required to move the [Flipper] to its highest position. /// @@ -97,7 +93,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { final sprite = await gameRef.loadSprite(spritePath); final spriteComponent = SpriteComponent( sprite: sprite, - size: Vector2(width, height), + size: size, anchor: Anchor.center, ); @@ -134,21 +130,21 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { final fixturesDef = []; final isLeft = side.isLeft; - final bigCircleShape = CircleShape()..radius = height / 2; + final bigCircleShape = CircleShape()..radius = 1.75; bigCircleShape.position.setValues( isLeft - ? -(width / 2) + bigCircleShape.radius - : (width / 2) - bigCircleShape.radius, + ? -(size.x / 2) + bigCircleShape.radius + : (size.x / 2) - bigCircleShape.radius, 0, ); final bigCircleFixtureDef = FixtureDef(bigCircleShape); fixturesDef.add(bigCircleFixtureDef); - final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2; + final smallCircleShape = CircleShape()..radius = 0.9; smallCircleShape.position.setValues( isLeft - ? (width / 2) - smallCircleShape.radius - : -(width / 2) + smallCircleShape.radius, + ? (size.x / 2) - smallCircleShape.radius + : -(size.x / 2) + smallCircleShape.radius, 0, ); final smallCircleFixtureDef = FixtureDef(smallCircleShape); @@ -227,8 +223,8 @@ class FlipperAnchor extends JointAnchor { }) { initialPosition = Vector2( flipper.side.isLeft - ? flipper.body.position.x - Flipper.width / 2 - : flipper.body.position.x + Flipper.width / 2, + ? flipper.body.position.x - Flipper.size.x / 2 + : flipper.body.position.x + Flipper.size.x / 2, flipper.body.position.y, ); } diff --git a/lib/game/components/sling_shot.dart b/lib/game/components/kicker.dart similarity index 86% rename from lib/game/components/sling_shot.dart rename to lib/game/components/kicker.dart index 53160213..e4b2824d 100644 --- a/lib/game/components/sling_shot.dart +++ b/lib/game/components/kicker.dart @@ -6,15 +6,15 @@ import 'package:flutter/material.dart'; import 'package:geometry/geometry.dart' as geometry show centroid; import 'package:pinball/game/game.dart'; -/// {@template sling_shot} +/// {@template kicker} /// Triangular [BodyType.static] body that propels the [Ball] towards the /// opposite side. /// -/// [SlingShot]s are usually positioned above each [Flipper]. -/// {@endtemplate sling_shot} -class SlingShot extends BodyComponent with InitialPosition { - /// {@macro sling_shot} - SlingShot({ +/// [Kicker]s are usually positioned above each [Flipper]. +/// {@endtemplate kicker} +class Kicker extends BodyComponent with InitialPosition { + /// {@macro kicker} + Kicker({ required BoardSide side, }) : _side = side { // TODO(alestiago): Use sprite instead of color when provided. @@ -23,14 +23,14 @@ class SlingShot extends BodyComponent with InitialPosition { ..style = PaintingStyle.fill; } - /// Whether the [SlingShot] is on the left or right side of the board. + /// Whether the [Kicker] is on the left or right side of the board. /// - /// A [SlingShot] with [BoardSide.left] propels the [Ball] to the right, - /// whereas a [SlingShot] with [BoardSide.right] propels the [Ball] to the + /// A [Kicker] with [BoardSide.left] propels the [Ball] to the right, + /// whereas a [Kicker] with [BoardSide.right] propels the [Ball] to the /// left. final BoardSide _side; - /// The size of the [SlingShot] body. + /// The size of the [Kicker] body. // TODO(alestiago): Use size from PositionedBodyComponent instead, // once a sprite is given. static final Vector2 size = Vector2(4, 10); @@ -78,7 +78,7 @@ class SlingShot extends BodyComponent with InitialPosition { final bottomLineFixtureDef = FixtureDef(bottomEdge)..friction = 0; fixturesDefs.add(bottomLineFixtureDef); - final kickerEdge = EdgeShape() + final bouncyEdge = EdgeShape() ..set( upperCircle.position + Vector2( @@ -92,11 +92,11 @@ class SlingShot extends BodyComponent with InitialPosition { ), ); - final kickerFixtureDef = FixtureDef(kickerEdge) + final bouncyFixtureDef = FixtureDef(bouncyEdge) // TODO(alestiago): Play with restitution value once game is bundled. ..restitution = 10.0 ..friction = 0; - fixturesDefs.add(kickerFixtureDef); + fixturesDefs.add(bouncyFixtureDef); // TODO(alestiago): Evaluate if there is value on centering the fixtures. final centroid = geometry.centroid( diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 61abaf1d..934cd8ac 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -1,4 +1,4 @@ -import 'package:flame/input.dart'; +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/services.dart'; import 'package:pinball/game/game.dart'; diff --git a/lib/leaderboard/bloc/leaderboard_bloc.dart b/lib/leaderboard/bloc/leaderboard_bloc.dart new file mode 100644 index 00000000..6542548d --- /dev/null +++ b/lib/leaderboard/bloc/leaderboard_bloc.dart @@ -0,0 +1,64 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; + +part 'leaderboard_event.dart'; +part 'leaderboard_state.dart'; + +/// {@template leaderboard_bloc} +/// Manages leaderboard events. +/// +/// Uses a [LeaderboardRepository] to request and update players participations. +/// {@endtemplate} +class LeaderboardBloc extends Bloc { + /// {@macro leaderboard_bloc} + LeaderboardBloc(this._leaderboardRepository) + : super(const LeaderboardState.initial()) { + on(_onTop10Fetched); + on(_onLeaderboardEntryAdded); + } + + final LeaderboardRepository _leaderboardRepository; + + Future _onTop10Fetched( + Top10Fetched event, + Emitter emit, + ) async { + emit(state.copyWith(status: LeaderboardStatus.loading)); + try { + final top10Leaderboard = + await _leaderboardRepository.fetchTop10Leaderboard(); + emit( + state.copyWith( + status: LeaderboardStatus.success, + leaderboard: top10Leaderboard, + ), + ); + } catch (error) { + emit(state.copyWith(status: LeaderboardStatus.error)); + addError(error); + } + } + + Future _onLeaderboardEntryAdded( + LeaderboardEntryAdded event, + Emitter emit, + ) async { + emit(state.copyWith(status: LeaderboardStatus.loading)); + try { + final ranking = + await _leaderboardRepository.addLeaderboardEntry(event.entry); + emit( + state.copyWith( + status: LeaderboardStatus.success, + ranking: ranking, + ), + ); + } catch (error) { + emit(state.copyWith(status: LeaderboardStatus.error)); + addError(error); + } + } +} diff --git a/lib/leaderboard/bloc/leaderboard_event.dart b/lib/leaderboard/bloc/leaderboard_event.dart new file mode 100644 index 00000000..34152163 --- /dev/null +++ b/lib/leaderboard/bloc/leaderboard_event.dart @@ -0,0 +1,36 @@ +part of 'leaderboard_bloc.dart'; + +/// {@template leaderboard_event} +/// Represents the events available for [LeaderboardBloc]. +/// {endtemplate} +abstract class LeaderboardEvent extends Equatable { + /// {@macro leaderboard_event} + const LeaderboardEvent(); +} + +/// {@template top_10_fetched} +/// Request the top 10 [LeaderboardEntry]s. +/// {endtemplate} +class Top10Fetched extends LeaderboardEvent { + /// {@macro top_10_fetched} + const Top10Fetched(); + + @override + List get props => []; +} + +/// {@template leaderboard_entry_added} +/// Writes a new [LeaderboardEntry]. +/// +/// Should be added when a player finishes a game. +/// {endtemplate} +class LeaderboardEntryAdded extends LeaderboardEvent { + /// {@macro leaderboard_entry_added} + const LeaderboardEntryAdded({required this.entry}); + + /// [LeaderboardEntry] to be written to the remote storage. + final LeaderboardEntry entry; + + @override + List get props => [entry]; +} diff --git a/lib/leaderboard/bloc/leaderboard_state.dart b/lib/leaderboard/bloc/leaderboard_state.dart new file mode 100644 index 00000000..20d68f0d --- /dev/null +++ b/lib/leaderboard/bloc/leaderboard_state.dart @@ -0,0 +1,59 @@ +// ignore_for_file: public_member_api_docs + +part of 'leaderboard_bloc.dart'; + +/// Defines the request status. +enum LeaderboardStatus { + /// Request is being loaded. + loading, + + /// Request was processed successfully and received a valid response. + success, + + /// Request was processed unsuccessfully and received an error. + error, +} + +/// {@template leaderboard_state} +/// Represents the state of the leaderboard. +/// {@endtemplate} +class LeaderboardState extends Equatable { + /// {@macro leaderboard_state} + const LeaderboardState({ + required this.status, + required this.ranking, + required this.leaderboard, + }); + + const LeaderboardState.initial() + : status = LeaderboardStatus.loading, + ranking = const LeaderboardRanking( + ranking: 0, + outOf: 0, + ), + leaderboard = const []; + + /// The current [LeaderboardStatus] of the state. + final LeaderboardStatus status; + + /// Rank of the current player. + final LeaderboardRanking ranking; + + /// List of top-ranked players. + final List leaderboard; + + @override + List get props => [status, ranking, leaderboard]; + + LeaderboardState copyWith({ + LeaderboardStatus? status, + LeaderboardRanking? ranking, + List? leaderboard, + }) { + return LeaderboardState( + status: status ?? this.status, + ranking: ranking ?? this.ranking, + leaderboard: leaderboard ?? this.leaderboard, + ); + } +} diff --git a/lib/leaderboard/leaderboard.dart b/lib/leaderboard/leaderboard.dart new file mode 100644 index 00000000..13d71e40 --- /dev/null +++ b/lib/leaderboard/leaderboard.dart @@ -0,0 +1 @@ +export 'bloc/leaderboard_bloc.dart'; diff --git a/pubspec.lock b/pubspec.lock index 71647cae..ac5fa36d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -182,21 +182,21 @@ packages: name: flame url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-releasecandidate.5" + version: "1.1.0-releasecandidate.6" flame_bloc: dependency: "direct main" description: name: flame_bloc url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-releasecandidate.5" + version: "1.2.0-releasecandidate.6" flame_forge2d: dependency: "direct main" description: name: flame_forge2d url: "https://pub.dartlang.org" source: hosted - version: "0.9.0-releasecandidate.5" + version: "0.9.0-releasecandidate.6" flame_test: dependency: "direct dev" description: @@ -237,7 +237,7 @@ packages: name: forge2d url: "https://pub.dartlang.org" source: hosted - version: "0.8.2" + version: "0.9.0" frontend_server_client: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 35d8190f..25c8fbb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,9 +10,9 @@ dependencies: bloc: ^8.0.2 cloud_firestore: ^3.1.10 equatable: ^2.0.3 - flame: ^1.1.0-releasecandidate.5 - flame_bloc: ^1.2.0-releasecandidate.5 - flame_forge2d: ^0.9.0-releasecandidate.5 + flame: ^1.1.0-releasecandidate.6 + flame_bloc: ^1.2.0-releasecandidate.6 + flame_forge2d: ^0.9.0-releasecandidate.6 flutter: sdk: flutter flutter_bloc: ^8.0.1 diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index d1057b47..04847dec 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_test.dart @@ -65,14 +65,14 @@ void main() { ); flameTester.test( - 'has two SlingShots', + 'has two Kickers', (game) async { final board = Board(); await game.ready(); await game.ensureAdd(board); - final slingShots = board.findNestedChildren(); - expect(slingShots.length, equals(2)); + final kickers = board.findNestedChildren(); + expect(kickers.length, equals(2)); }, ); diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart index e6e9ba23..64d2f77b 100644 --- a/test/game/components/flipper_test.dart +++ b/test/game/components/flipper_test.dart @@ -282,7 +282,7 @@ void main() { final flipperAnchor = FlipperAnchor(flipper: flipper); await game.ensureAdd(flipperAnchor); - expect(flipperAnchor.body.position.x, equals(-Flipper.width / 2)); + expect(flipperAnchor.body.position.x, equals(-Flipper.size.x / 2)); }, ); @@ -297,7 +297,7 @@ void main() { final flipperAnchor = FlipperAnchor(flipper: flipper); await game.ensureAdd(flipperAnchor); - expect(flipperAnchor.body.position.x, equals(Flipper.width / 2)); + expect(flipperAnchor.body.position.x, equals(Flipper.size.x / 2)); }, ); }); diff --git a/test/game/components/sling_shot_test.dart b/test/game/components/kicker_test.dart similarity index 65% rename from test/game/components/sling_shot_test.dart rename to test/game/components/kicker_test.dart index 2bfb2355..211ff8ad 100644 --- a/test/game/components/sling_shot_test.dart +++ b/test/game/components/kicker_test.dart @@ -6,43 +6,43 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; void main() { - group('SlingShot', () { + group('Kicker', () { // TODO(alestiago): Include golden tests for left and right. final flameTester = FlameTester(Forge2DGame.new); flameTester.test( 'loads correctly', (game) async { - final slingShot = SlingShot( + final kicker = Kicker( side: BoardSide.left, ); - await game.ensureAdd(slingShot); + await game.ensureAdd(kicker); - expect(game.contains(slingShot), isTrue); + expect(game.contains(kicker), isTrue); }, ); flameTester.test( 'body is static', (game) async { - final slingShot = SlingShot( + final kicker = Kicker( side: BoardSide.left, ); - await game.ensureAdd(slingShot); + await game.ensureAdd(kicker); - expect(slingShot.body.bodyType, equals(BodyType.static)); + expect(kicker.body.bodyType, equals(BodyType.static)); }, ); flameTester.test( 'has restitution', (game) async { - final slingShot = SlingShot( + final kicker = Kicker( side: BoardSide.left, ); - await game.ensureAdd(slingShot); + await game.ensureAdd(kicker); - final totalRestitution = slingShot.body.fixtures.fold( + final totalRestitution = kicker.body.fixtures.fold( 0, (total, fixture) => total + fixture.restitution, ); @@ -53,12 +53,12 @@ void main() { flameTester.test( 'has no friction', (game) async { - final slingShot = SlingShot( + final kicker = Kicker( side: BoardSide.left, ); - await game.ensureAdd(slingShot); + await game.ensureAdd(kicker); - final totalFriction = slingShot.body.fixtures.fold( + final totalFriction = kicker.body.fixtures.fold( 0, (total, fixture) => total + fixture.friction, ); diff --git a/test/leaderboard/bloc/leaderboard_bloc_test.dart b/test/leaderboard/bloc/leaderboard_bloc_test.dart new file mode 100644 index 00000000..c44f7d3a --- /dev/null +++ b/test/leaderboard/bloc/leaderboard_bloc_test.dart @@ -0,0 +1,166 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; + +class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} + +void main() { + group('LeaderboardBloc', () { + late LeaderboardRepository leaderboardRepository; + + setUp(() { + leaderboardRepository = MockLeaderboardRepository(); + }); + + test('initial state has state loading no ranking and empty leaderboard', + () { + final bloc = LeaderboardBloc(leaderboardRepository); + expect(bloc.state.status, equals(LeaderboardStatus.loading)); + expect(bloc.state.ranking.ranking, equals(0)); + expect(bloc.state.ranking.outOf, equals(0)); + expect(bloc.state.leaderboard.isEmpty, isTrue); + }); + + group('Top10Fetched', () { + const top10Scores = [ + 2500, + 2200, + 2200, + 2000, + 1800, + 1400, + 1300, + 1000, + 600, + 300, + 100, + ]; + + final top10Leaderboard = top10Scores + .map( + (score) => LeaderboardEntry( + playerInitials: 'user$score', + score: score, + character: CharacterType.dash, + ), + ) + .toList(); + + blocTest( + 'emits [loading, success] statuses ' + 'when fetchTop10Leaderboard succeeds', + setUp: () { + when(() => leaderboardRepository.fetchTop10Leaderboard()).thenAnswer( + (_) async => top10Leaderboard, + ); + }, + build: () => LeaderboardBloc(leaderboardRepository), + act: (bloc) => bloc.add(Top10Fetched()), + expect: () => [ + LeaderboardState.initial(), + isA() + ..having( + (element) => element.status, + 'status', + equals(LeaderboardStatus.success), + ) + ..having( + (element) => element.leaderboard.length, + 'leaderboard', + equals(top10Leaderboard.length), + ) + ], + verify: (_) => + verify(() => leaderboardRepository.fetchTop10Leaderboard()) + .called(1), + ); + + blocTest( + 'emits [loading, error] statuses ' + 'when fetchTop10Leaderboard fails', + setUp: () { + when(() => leaderboardRepository.fetchTop10Leaderboard()).thenThrow( + Exception(), + ); + }, + build: () => LeaderboardBloc(leaderboardRepository), + act: (bloc) => bloc.add(Top10Fetched()), + expect: () => [ + LeaderboardState.initial(), + LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), + ], + verify: (_) => + verify(() => leaderboardRepository.fetchTop10Leaderboard()) + .called(1), + errors: () => [isA()], + ); + }); + + group('LeaderboardEntryAdded', () { + final leaderboardEntry = LeaderboardEntry( + playerInitials: 'ABC', + score: 1500, + character: CharacterType.dash, + ); + + final ranking = LeaderboardRanking(ranking: 3, outOf: 4); + + blocTest( + 'emits [loading, success] statuses ' + 'when addLeaderboardEntry succeeds', + setUp: () { + when( + () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), + ).thenAnswer( + (_) async => ranking, + ); + }, + build: () => LeaderboardBloc(leaderboardRepository), + act: (bloc) => bloc.add(LeaderboardEntryAdded(entry: leaderboardEntry)), + expect: () => [ + LeaderboardState.initial(), + isA() + ..having( + (element) => element.status, + 'status', + equals(LeaderboardStatus.success), + ) + ..having( + (element) => element.ranking, + 'ranking', + equals(ranking), + ) + ], + verify: (_) => verify( + () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), + ).called(1), + ); + + blocTest( + 'emits [loading, error] statuses ' + 'when addLeaderboardEntry fails', + setUp: () { + when( + () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), + ).thenThrow( + Exception(), + ); + }, + build: () => LeaderboardBloc(leaderboardRepository), + act: (bloc) => bloc.add(LeaderboardEntryAdded(entry: leaderboardEntry)), + expect: () => [ + LeaderboardState.initial(), + LeaderboardState.initial().copyWith(status: LeaderboardStatus.error), + ], + verify: (_) => verify( + () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), + ).called(1), + errors: () => [isA()], + ); + }); + }); +} diff --git a/test/leaderboard/bloc/leaderboard_event_test.dart b/test/leaderboard/bloc/leaderboard_event_test.dart new file mode 100644 index 00000000..f74296af --- /dev/null +++ b/test/leaderboard/bloc/leaderboard_event_test.dart @@ -0,0 +1,41 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; + +void main() { + group('GameEvent', () { + group('Top10Fetched', () { + test('can be instantiated', () { + expect(const Top10Fetched(), isNotNull); + }); + + test('supports value equality', () { + expect( + Top10Fetched(), + equals(const Top10Fetched()), + ); + }); + }); + + group('LeaderboardEntryAdded', () { + const leaderboardEntry = LeaderboardEntry( + playerInitials: 'ABC', + score: 1500, + character: CharacterType.dash, + ); + + test('can be instantiated', () { + expect(const LeaderboardEntryAdded(entry: leaderboardEntry), isNotNull); + }); + + test('supports value equality', () { + expect( + LeaderboardEntryAdded(entry: leaderboardEntry), + equals(const LeaderboardEntryAdded(entry: leaderboardEntry)), + ); + }); + }); + }); +} diff --git a/test/leaderboard/bloc/leaderboard_state_test.dart b/test/leaderboard/bloc/leaderboard_state_test.dart new file mode 100644 index 00000000..6ff5df13 --- /dev/null +++ b/test/leaderboard/bloc/leaderboard_state_test.dart @@ -0,0 +1,70 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; + +void main() { + group('LeaderboardState', () { + test('supports value equality', () { + expect( + LeaderboardState.initial(), + equals( + LeaderboardState.initial(), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + LeaderboardState.initial(), + isNotNull, + ); + }); + }); + + group('copyWith', () { + const leaderboardEntry = LeaderboardEntry( + playerInitials: 'ABC', + score: 1500, + character: CharacterType.dash, + ); + + test( + 'copies correctly ' + 'when no argument specified', + () { + const leaderboardState = LeaderboardState.initial(); + expect( + leaderboardState.copyWith(), + equals(leaderboardState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + const leaderboardState = LeaderboardState.initial(); + final otherLeaderboardState = LeaderboardState( + status: LeaderboardStatus.success, + ranking: LeaderboardRanking(ranking: 0, outOf: 0), + leaderboard: const [leaderboardEntry], + ); + expect(leaderboardState, isNot(equals(otherLeaderboardState))); + + expect( + leaderboardState.copyWith( + status: otherLeaderboardState.status, + ranking: otherLeaderboardState.ranking, + leaderboard: otherLeaderboardState.leaderboard, + ), + equals(otherLeaderboardState), + ); + }, + ); + }); + }); +}