Merge branch 'main' into feat/leaderboard_screen

pull/51/head
RuiAlonso 4 years ago
commit b033373bb7

@ -0,0 +1,18 @@
name: leaderboard_repository
on:
push:
paths:
- "packages/leaderboard_repository/**"
- ".github/workflows/leaderboard_repository.yaml"
pull_request:
paths:
- "packages/leaderboard_repository/**"
- ".github/workflows/leaderboard_repository.yaml"
jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
working_directory: packages/leaderboard_repository

2
.gitignore vendored

@ -126,4 +126,6 @@ app.*.map.json
!.idea/dictionaries/
!.idea/runConfigurations/
# Firebase related
.firebase
web/__/firebase/init.js

@ -8,29 +8,38 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/landing/landing.dart';
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
const App({Key? key, required LeaderboardRepository leaderboardRepository})
: _leaderboardRepository = leaderboardRepository,
super(key: key);
final LeaderboardRepository _leaderboardRepository;
@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 RepositoryProvider.value(
value: _leaderboardRepository,
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(),
);
}
}

@ -11,6 +11,7 @@ import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/widgets.dart';
class AppBlocObserver extends BlocObserver {
@ -27,7 +28,10 @@ class AppBlocObserver extends BlocObserver {
}
}
Future<void> bootstrap(FutureOr<Widget> Function() builder) async {
Future<void> bootstrap(
Future<Widget> Function(FirebaseFirestore firestore) builder,
) async {
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (details) {
log(details.exceptionAsString(), stackTrace: details.stack);
};
@ -35,7 +39,7 @@ Future<void> bootstrap(FutureOr<Widget> Function() builder) async {
await runZonedGuarded(
() async {
await BlocOverrides.runZoned(
() async => runApp(await builder()),
() async => runApp(await builder(FirebaseFirestore.instance)),
blocObserver: AppBlocObserver(),
);
},

@ -1,15 +1,87 @@
import 'package:flame/components.dart';
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.
/// {entemplate}
class Board extends Component {
/// {@macro board}
Board({required Vector2 size}) : _size = size;
final Vector2 _size;
@override
Future<void> onLoad() async {
// TODO(alestiago): adjust positioning once sprites are added.
final bottomGroup = _BottomGroup(
position: Vector2(
_size.x / 2,
_size.y / 1.25,
),
spacing: 2,
);
final dashForest = _FlutterForest(
position: Vector2(
_size.x / 1.25,
_size.y / 4.25,
),
);
await addAll([
bottomGroup,
dashForest,
]);
}
}
/// {@template flutter_forest}
/// Area positioned at the top right of the [Board] where the [Ball]
/// can bounce off [RoundBumper]s.
/// {@endtemplate}
class _FlutterForest extends Component {
/// {@macro flutter_forest}
_FlutterForest({
required this.position,
});
final Vector2 position;
@override
Future<void> onLoad() async {
// TODO(alestiago): adjust positioning once sprites are added.
// TODO(alestiago): Use [NestBumper] instead of [RoundBumper] once provided.
final smallLeftNest = RoundBumper(
radius: 1,
points: 10,
)..initialPosition = position + Vector2(-4.8, 2.8);
final smallRightNest = RoundBumper(
radius: 1,
points: 10,
)..initialPosition = position + Vector2(0.5, -5.5);
final bigNest = RoundBumper(
radius: 2,
points: 20,
)..initialPosition = position;
await addAll([
smallLeftNest,
smallRightNest,
bigNest,
]);
}
}
/// {@template bottom_group}
/// Grouping of the board's bottom [Component]s.
///
/// The bottom [Component]s are the [Flipper]s and the [Baseboard]s.
/// The [_BottomGroup] consists of[Flipper]s, [Baseboard]s and [SlingShot]s.
/// {@endtemplate}
// TODO(alestiago): Consider renaming once entire Board is defined.
class BottomGroup extends Component {
class _BottomGroup extends Component {
/// {@macro bottom_group}
BottomGroup({
_BottomGroup({
required this.position,
required this.spacing,
});
@ -17,7 +89,7 @@ class BottomGroup extends Component {
/// The amount of space between the line of symmetry.
final double spacing;
/// The position of this [BottomGroup].
/// The position of this [_BottomGroup].
final Vector2 position;
@override
@ -37,7 +109,7 @@ class BottomGroup extends Component {
}
/// {@template bottom_group_side}
/// Group with one side of [BottomGroup]'s symmetric [Component]s.
/// Group with one side of [_BottomGroup]'s symmetric [Component]s.
///
/// For example, [Flipper]s are symmetric components.
/// {@endtemplate}

@ -203,9 +203,6 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
// TODO(alestiago): Check why false cancels the event for other components.
// Investigate why return is of type [bool] expected instead of a type
// [KeyEventResult].
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
@ -214,7 +211,7 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
_moveDown();
}
return true;
return false;
}
}

@ -57,9 +57,6 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.keyS,
];
// TODO(alestiago): Check why false cancels the event for other components.
// Investigate why return is of type [bool] expected instead of a type
// [KeyEventResult].
if (!keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
@ -68,7 +65,7 @@ class Plunger extends BodyComponent with KeyboardHandler, InitialPosition {
_release();
}
return true;
return false;
}
/// Anchors the [Plunger] to the [PrismaticJoint] that controls its vertical

@ -49,19 +49,19 @@ class PinballGame extends Forge2DGame
);
unawaited(_addBonusWord());
unawaited(
add(
BottomGroup(
position: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2,
camera.viewport.effectiveSize.y / 1.25,
),
),
spacing: 2,
unawaited(_addBoard());
}
Future<void> _addBoard() async {
final board = Board(
size: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x,
camera.viewport.effectiveSize.y,
),
),
);
await add(board);
}
Future<void> _addBonusWord() async {

@ -5,9 +5,13 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart';
void main() {
bootstrap(() => const App());
bootstrap((firestore) async {
final leaderboardRepository = LeaderboardRepository(firestore);
return App(leaderboardRepository: leaderboardRepository);
});
}

@ -5,9 +5,13 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart';
void main() {
bootstrap(() => const App());
bootstrap((firestore) async {
final leaderboardRepository = LeaderboardRepository(firestore);
return App(leaderboardRepository: leaderboardRepository);
});
}

@ -5,9 +5,13 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/app/app.dart';
import 'package:pinball/bootstrap.dart';
void main() {
bootstrap(() => const App());
bootstrap((firestore) async {
final leaderboardRepository = LeaderboardRepository(firestore);
return App(leaderboardRepository: leaderboardRepository);
});
}

@ -0,0 +1,39 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# VSCode related
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json

@ -0,0 +1,11 @@
# leaderboard_repository
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
Repository to access leaderboard data in Firebase Cloud Firestore.
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis

@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml

@ -0,0 +1,4 @@
library leaderboard_repository;
export 'src/leaderboard_repository.dart';
export 'src/models/models.dart';

@ -0,0 +1,153 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
/// {@template leaderboard_exception}
/// Base exception for leaderboard repository failures.
/// {@endtemplate}
abstract class LeaderboardException implements Exception {
/// {@macro leaderboard_exception}
const LeaderboardException(this.error, this.stackTrace);
/// The error that was caught.
final Object error;
/// The Stacktrace associated with the [error].
final StackTrace stackTrace;
}
/// {@template leaderboard_deserialization_exception}
/// Exception thrown when leaderboard data cannot be deserialized in the
/// expected way.
/// {@endtemplate}
class LeaderboardDeserializationException extends LeaderboardException {
/// {@macro leaderboard_deserialization_exception}
const LeaderboardDeserializationException(
Object error,
StackTrace stackTrace,
) : super(
error,
stackTrace,
);
}
/// {@template fetch_top_10_leaderboard_exception}
/// Exception thrown when failure occurs while fetching top 10 leaderboard.
/// {@endtemplate}
class FetchTop10LeaderboardException extends LeaderboardException {
/// {@macro fetch_top_10_leaderboard_exception}
const FetchTop10LeaderboardException(
Object error,
StackTrace stackTrace,
) : super(
error,
stackTrace,
);
}
/// {@template add_leaderboard_entry_exception}
/// Exception thrown when failure occurs while adding entry to leaderboard.
/// {@endtemplate}
class AddLeaderboardEntryException extends LeaderboardException {
/// {@macro add_leaderboard_entry_exception}
const AddLeaderboardEntryException(
Object error,
StackTrace stackTrace,
) : super(
error,
stackTrace,
);
}
/// {@template fetch_player_ranking_exception}
/// Exception thrown when failure occurs while fetching player ranking.
/// {@endtemplate}
class FetchPlayerRankingException extends LeaderboardException {
/// {@macro fetch_player_ranking_exception}
const FetchPlayerRankingException(
Object error,
StackTrace stackTrace,
) : super(
error,
stackTrace,
);
}
/// {@template leaderboard_repository}
/// Repository to access leaderboard data in Firebase Cloud Firestore.
/// {@endtemplate}
class LeaderboardRepository {
/// {@macro leaderboard_repository}
const LeaderboardRepository(
FirebaseFirestore firebaseFirestore,
) : _firebaseFirestore = firebaseFirestore;
final FirebaseFirestore _firebaseFirestore;
/// Acquires top 10 [LeaderboardEntry]s.
Future<List<LeaderboardEntry>> fetchTop10Leaderboard() async {
final leaderboardEntries = <LeaderboardEntry>[];
late List<QueryDocumentSnapshot> documents;
try {
final querySnapshot = await _firebaseFirestore
.collection('leaderboard')
.orderBy('score')
.limit(10)
.get();
documents = querySnapshot.docs;
} on Exception catch (error, stackTrace) {
throw FetchTop10LeaderboardException(error, stackTrace);
}
for (final document in documents) {
final data = document.data() as Map<String, dynamic>?;
if (data != null) {
try {
leaderboardEntries.add(LeaderboardEntry.fromJson(data));
} catch (error, stackTrace) {
throw LeaderboardDeserializationException(error, stackTrace);
}
}
}
return leaderboardEntries;
}
/// Adds player's score entry to the leaderboard and gets their
/// [LeaderboardRanking].
Future<LeaderboardRanking> addLeaderboardEntry(LeaderboardEntry entry) async {
late DocumentReference entryReference;
try {
entryReference = await _firebaseFirestore
.collection('leaderboard')
.add(entry.toJson());
} on Exception catch (error, stackTrace) {
throw AddLeaderboardEntryException(error, stackTrace);
}
try {
final querySnapshot = await _firebaseFirestore
.collection('leaderboard')
.orderBy('score')
.get();
// TODO(allisonryan0002): see if we can find a more performant solution.
final documents = querySnapshot.docs;
final ranking = documents.indexWhere(
(document) => document.id == entryReference.id,
) +
1;
if (ranking > 0) {
return LeaderboardRanking(ranking: ranking, outOf: documents.length);
} else {
throw FetchPlayerRankingException(
'Player score could not be found and ranking cannot be provided.',
StackTrace.current,
);
}
} on Exception catch (error, stackTrace) {
throw FetchPlayerRankingException(error, stackTrace);
}
}
}

@ -0,0 +1,80 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'leaderboard_entry.g.dart';
/// Google character type associated with a [LeaderboardEntry].
enum CharacterType {
/// Dash character.
dash,
/// Sparky character.
sparky,
/// Android character.
android,
/// Dino character.
dino,
}
/// {@template leaderboard_entry}
/// A model representing a leaderboard entry containing the player's initials,
/// score, and chosen character.
///
/// Stored in Firestore `leaderboard` collection.
///
/// Example:
/// ```json
/// {
/// "playerInitials" : "ABC",
/// "score" : 1500,
/// "character" : "dash"
/// }
/// ```
/// {@endtemplate}
@JsonSerializable()
class LeaderboardEntry extends Equatable {
/// {@macro leaderboard_entry}
const LeaderboardEntry({
required this.playerInitials,
required this.score,
required this.character,
});
/// Factory which converts a [Map] into a [LeaderboardEntry].
factory LeaderboardEntry.fromJson(Map<String, dynamic> json) {
return _$LeaderboardEntryFromJson(json);
}
/// Converts the [LeaderboardEntry] to [Map].
Map<String, dynamic> toJson() => _$LeaderboardEntryToJson(this);
/// Player's chosen initials for [LeaderboardEntry].
///
/// Example: 'ABC'.
@JsonKey(name: 'playerInitials')
final String playerInitials;
/// Score for [LeaderboardEntry].
///
/// Example: 1500.
@JsonKey(name: 'score')
final int score;
/// [CharacterType] for [LeaderboardEntry].
///
/// Example: [CharacterType.dash].
@JsonKey(name: 'character')
final CharacterType character;
/// An empty [LeaderboardEntry] object.
static const empty = LeaderboardEntry(
playerInitials: '',
score: 0,
character: CharacterType.dash,
);
@override
List<Object?> get props => [playerInitials, score, character];
}

@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'leaderboard_entry.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
LeaderboardEntry _$LeaderboardEntryFromJson(Map<String, dynamic> json) =>
LeaderboardEntry(
playerInitials: json['playerInitials'] as String,
score: json['score'] as int,
character: $enumDecode(_$CharacterTypeEnumMap, json['character']),
);
Map<String, dynamic> _$LeaderboardEntryToJson(LeaderboardEntry instance) =>
<String, dynamic>{
'playerInitials': instance.playerInitials,
'score': instance.score,
'character': _$CharacterTypeEnumMap[instance.character],
};
const _$CharacterTypeEnumMap = {
CharacterType.dash: 'dash',
CharacterType.sparky: 'sparky',
CharacterType.android: 'android',
CharacterType.dino: 'dino',
};

@ -0,0 +1,20 @@
import 'package:equatable/equatable.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
/// {@template leaderboard_ranking}
/// Contains [ranking] for a single [LeaderboardEntry] and the number of players
/// the [ranking] is [outOf].
/// {@endtemplate}
class LeaderboardRanking extends Equatable {
/// {@macro leaderboard_ranking}
const LeaderboardRanking({required this.ranking, required this.outOf});
/// Place ranking by score for a [LeaderboardEntry].
final int ranking;
/// Number of [LeaderboardEntry]s at the time of score entry.
final int outOf;
@override
List<Object> get props => [ranking, outOf];
}

@ -0,0 +1,2 @@
export 'leaderboard_entry.dart';
export 'leaderboard_ranking.dart';

@ -0,0 +1,23 @@
name: leaderboard_repository
description: Repository to access leaderboard data in Firebase Cloud Firestore.
version: 1.0.0+1
publish_to: none
environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
cloud_firestore: ^3.1.10
equatable: ^2.0.3
flutter:
sdk: flutter
json_annotation: ^4.4.0
dev_dependencies:
build_runner: ^2.1.8
flutter_test:
sdk: flutter
json_serializable: ^6.1.5
mocktail: ^0.2.0
test: ^1.19.2
very_good_analysis: ^2.4.0

@ -0,0 +1,226 @@
// ignore_for_file: prefer_const_constructors, subtype_of_sealed_class
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
class MockFirebaseFirestore extends Mock implements FirebaseFirestore {}
class MockCollectionReference extends Mock
implements CollectionReference<Map<String, dynamic>> {}
class MockQuery extends Mock implements Query<Map<String, dynamic>> {}
class MockQuerySnapshot extends Mock
implements QuerySnapshot<Map<String, dynamic>> {}
class MockQueryDocumentSnapshot extends Mock
implements QueryDocumentSnapshot<Map<String, dynamic>> {}
class MockDocumentReference extends Mock
implements DocumentReference<Map<String, dynamic>> {}
void main() {
group('LeaderboardRepository', () {
late FirebaseFirestore firestore;
setUp(() {
firestore = MockFirebaseFirestore();
});
test('can be instantiated', () {
expect(LeaderboardRepository(firestore), isNotNull);
});
group('fetchTop10Leaderboard', () {
late LeaderboardRepository leaderboardRepository;
late CollectionReference<Map<String, dynamic>> collectionReference;
late Query<Map<String, dynamic>> query;
late QuerySnapshot<Map<String, dynamic>> querySnapshot;
late List<QueryDocumentSnapshot<Map<String, dynamic>>>
queryDocumentSnapshots;
final 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();
setUp(() {
leaderboardRepository = LeaderboardRepository(firestore);
collectionReference = MockCollectionReference();
query = MockQuery();
querySnapshot = MockQuerySnapshot();
queryDocumentSnapshots = top10Scores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash',
'playerInitials': 'user$score',
'score': score
});
return queryDocumentSnapshot;
}).toList();
when(() => firestore.collection('leaderboard'))
.thenAnswer((_) => collectionReference);
when(() => collectionReference.orderBy('score'))
.thenAnswer((_) => query);
when(() => query.limit(10)).thenAnswer((_) => query);
when(query.get).thenAnswer((_) async => querySnapshot);
when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots);
});
test(
'returns top 10 entries when '
'retrieving information from firestore succeeds', () async {
final top10LeaderboardResults =
await leaderboardRepository.fetchTop10Leaderboard();
expect(top10LeaderboardResults, equals(top10Leaderboard));
});
test(
'throws FetchTop10LeaderboardException when Exception occurs '
'when trying to retrieve information from firestore', () async {
when(() => firestore.collection('leaderboard')).thenThrow(Exception());
expect(
() => leaderboardRepository.fetchTop10Leaderboard(),
throwsA(isA<FetchTop10LeaderboardException>()),
);
});
test(
'throws LeaderboardDeserializationException when Exception occurs '
'during deserialization', () async {
final top10LeaderboardDataMalformed = <String, dynamic>{
'playerInitials': 'ABC',
'score': 1500,
};
final queryDocumentSnapshot = MockQueryDocumentSnapshot();
when(() => querySnapshot.docs).thenReturn([queryDocumentSnapshot]);
when(queryDocumentSnapshot.data)
.thenReturn(top10LeaderboardDataMalformed);
expect(
() => leaderboardRepository.fetchTop10Leaderboard(),
throwsA(isA<LeaderboardDeserializationException>()),
);
});
});
group('addLeaderboardEntry', () {
late LeaderboardRepository leaderboardRepository;
late CollectionReference<Map<String, dynamic>> collectionReference;
late DocumentReference<Map<String, dynamic>> documentReference;
late Query<Map<String, dynamic>> query;
late QuerySnapshot<Map<String, dynamic>> querySnapshot;
late List<QueryDocumentSnapshot<Map<String, dynamic>>>
queryDocumentSnapshots;
const entryScore = 1500;
final leaderboardScores = [
2500,
2200,
entryScore,
1000,
];
final leaderboardEntry = LeaderboardEntry(
playerInitials: 'ABC',
score: entryScore,
character: CharacterType.dash,
);
const entryDocumentId = 'id$entryScore';
final ranking = LeaderboardRanking(ranking: 3, outOf: 4);
setUp(() {
leaderboardRepository = LeaderboardRepository(firestore);
collectionReference = MockCollectionReference();
documentReference = MockDocumentReference();
query = MockQuery();
querySnapshot = MockQuerySnapshot();
queryDocumentSnapshots = leaderboardScores.map((score) {
final queryDocumentSnapshot = MockQueryDocumentSnapshot();
when(queryDocumentSnapshot.data).thenReturn(<String, dynamic>{
'character': 'dash',
'username': 'user$score',
'score': score
});
when(() => queryDocumentSnapshot.id).thenReturn('id$score');
return queryDocumentSnapshot;
}).toList();
when(() => firestore.collection('leaderboard'))
.thenAnswer((_) => collectionReference);
when(() => collectionReference.add(any()))
.thenAnswer((_) async => documentReference);
when(() => collectionReference.orderBy('score'))
.thenAnswer((_) => query);
when(query.get).thenAnswer((_) async => querySnapshot);
when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots);
when(() => documentReference.id).thenReturn(entryDocumentId);
});
test(
'adds leaderboard entry and returns player ranking when '
'firestore operations succeed', () async {
final rankingResult =
await leaderboardRepository.addLeaderboardEntry(leaderboardEntry);
expect(rankingResult, equals(ranking));
});
test(
'throws AddLeaderboardEntryException when Exception occurs '
'when trying to add entry to firestore', () async {
when(() => firestore.collection('leaderboard')).thenThrow(Exception());
expect(
() => leaderboardRepository.addLeaderboardEntry(leaderboardEntry),
throwsA(isA<AddLeaderboardEntryException>()),
);
});
test(
'throws FetchPlayerRankingException when Exception occurs '
'when trying to retrieve information from firestore', () async {
when(() => collectionReference.orderBy('score')).thenThrow(Exception());
expect(
() => leaderboardRepository.addLeaderboardEntry(leaderboardEntry),
throwsA(isA<FetchPlayerRankingException>()),
);
});
test(
'throws FetchPlayerRankingException when score cannot be found '
'in firestore leaderboard data', () async {
when(() => documentReference.id).thenReturn('nonexistentDocumentId');
expect(
() => leaderboardRepository.addLeaderboardEntry(leaderboardEntry),
throwsA(isA<FetchPlayerRankingException>()),
);
});
});
});
}

@ -0,0 +1,41 @@
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:test/test.dart';
void main() {
group('LeaderboardEntry', () {
const data = <String, dynamic>{
'playerInitials': 'ABC',
'score': 1500,
'character': 'dash',
};
const leaderboardEntry = LeaderboardEntry(
playerInitials: 'ABC',
score: 1500,
character: CharacterType.dash,
);
test('can be instantiated', () {
const leaderboardEntry = LeaderboardEntry.empty;
expect(leaderboardEntry, isNotNull);
});
test('supports value equality.', () {
const leaderboardEntry = LeaderboardEntry.empty;
const leaderboardEntry2 = LeaderboardEntry.empty;
expect(leaderboardEntry, equals(leaderboardEntry2));
});
test('can be converted to json', () {
expect(leaderboardEntry.toJson(), equals(data));
});
test('can be obtained from json', () {
final leaderboardEntryFrom = LeaderboardEntry.fromJson(data);
expect(leaderboardEntry, equals(leaderboardEntryFrom));
});
});
}

@ -0,0 +1,19 @@
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:test/test.dart';
void main() {
group('LeaderboardRanking', () {
test('can be instantiated', () {
const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1);
expect(leaderboardRanking, isNotNull);
});
test('supports value equality.', () {
const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1);
const leaderboardRanking2 = LeaderboardRanking(ranking: 1, outOf: 1);
expect(leaderboardRanking, equals(leaderboardRanking2));
});
});
}

@ -78,6 +78,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
cloud_firestore:
dependency: "direct main"
description:
name: cloud_firestore
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.10"
cloud_firestore_platform_interface:
dependency: transitive
description:
name: cloud_firestore_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "5.5.1"
cloud_firestore_web:
dependency: transitive
description:
name: cloud_firestore_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.6.10"
collection:
dependency: transitive
description:
@ -134,6 +155,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
firebase_core:
dependency: transitive
description:
name: firebase_core
url: "https://pub.dartlang.org"
source: hosted
version: "1.13.1"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.5"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.1"
flame:
dependency: "direct main"
description:
@ -184,6 +226,11 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
forge2d:
dependency: transitive
description:
@ -246,7 +293,21 @@ packages:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.4"
version: "0.6.3"
json_annotation:
dependency: transitive
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "4.4.0"
leaderboard_repository:
dependency: "direct main"
description:
path: "packages/leaderboard_repository"
relative: true
source: path
version: "1.0.0+1"
logging:
dependency: transitive
description:
@ -338,6 +399,13 @@ packages:
relative: true
source: path
version: "1.0.0+1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
pool:
dependency: transitive
description:

@ -8,6 +8,7 @@ environment:
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
@ -20,6 +21,8 @@ dependencies:
geometry:
path: packages/geometry
intl: ^0.17.0
leaderboard_repository:
path: packages/leaderboard_repository
pinball_theme:
path: packages/pinball_theme

@ -6,13 +6,25 @@
// https://opensource.org/licenses/MIT.
import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/app/app.dart';
import 'package:pinball/landing/landing.dart';
class MockLeaderboardRepository extends Mock implements LeaderboardRepository {}
void main() {
group('App', () {
late LeaderboardRepository leaderboardRepository;
setUp(() {
leaderboardRepository = MockLeaderboardRepository();
});
testWidgets('renders LandingPage', (tester) async {
await tester.pumpWidget(const App());
await tester.pumpWidget(
App(leaderboardRepository: leaderboardRepository),
);
expect(find.byType(LandingPage), findsOneWidget);
});
});

@ -11,15 +11,15 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new);
group('BottomGroup', () {
group('Board', () {
flameTester.test(
'loads correctly',
(game) async {
final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0);
final board = Board(size: Vector2.all(500));
await game.ready();
await game.ensureAdd(bottomGroup);
await game.ensureAdd(board);
expect(game.contains(bottomGroup), isTrue);
expect(game.contains(board), isTrue);
},
);
@ -27,11 +27,11 @@ void main() {
flameTester.test(
'has one left flipper',
(game) async {
final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0);
final board = Board(size: Vector2.all(500));
await game.ready();
await game.ensureAdd(bottomGroup);
await game.ensureAdd(board);
final leftFlippers = bottomGroup.findNestedChildren<Flipper>(
final leftFlippers = board.findNestedChildren<Flipper>(
condition: (flipper) => flipper.side.isLeft,
);
expect(leftFlippers.length, equals(1));
@ -41,11 +41,11 @@ void main() {
flameTester.test(
'has one right flipper',
(game) async {
final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0);
final board = Board(size: Vector2.all(500));
await game.ready();
await game.ensureAdd(bottomGroup);
await game.ensureAdd(board);
final rightFlippers = bottomGroup.findNestedChildren<Flipper>(
final rightFlippers = board.findNestedChildren<Flipper>(
condition: (flipper) => flipper.side.isRight,
);
expect(rightFlippers.length, equals(1));
@ -55,11 +55,11 @@ void main() {
flameTester.test(
'has two Baseboards',
(game) async {
final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0);
final board = Board(size: Vector2.all(500));
await game.ready();
await game.ensureAdd(bottomGroup);
await game.ensureAdd(board);
final baseboards = bottomGroup.findNestedChildren<Baseboard>();
final baseboards = board.findNestedChildren<Baseboard>();
expect(baseboards.length, equals(2));
},
);
@ -67,14 +67,27 @@ void main() {
flameTester.test(
'has two SlingShots',
(game) async {
final bottomGroup = BottomGroup(position: Vector2.zero(), spacing: 0);
final board = Board(size: Vector2.all(500));
await game.ready();
await game.ensureAdd(bottomGroup);
await game.ensureAdd(board);
final slingShots = bottomGroup.findNestedChildren<SlingShot>();
final slingShots = board.findNestedChildren<SlingShot>();
expect(slingShots.length, equals(2));
},
);
flameTester.test(
'has three RoundBumpers',
(game) async {
// TODO(alestiago): change to [NestBumpers] once provided.
final board = Board(size: Vector2.all(500));
await game.ready();
await game.ensureAdd(board);
final roundBumpers = board.findNestedChildren<RoundBumper>();
expect(roundBumpers.length, equals(3));
},
);
});
});
}

@ -54,10 +54,10 @@ void main() {
},
);
flameTester.test('has only one BottomGroup', (game) async {
flameTester.test('has one Board', (game) async {
await game.ready();
expect(
game.children.whereType<BottomGroup>().length,
game.children.whereType<Board>().length,
equals(1),
);
});

File diff suppressed because one or more lines are too long

@ -0,0 +1,11 @@
if (typeof firebase === 'undefined') throw new Error('hosting/init-error: Firebase SDK not detected. You must include it before /__/firebase/init.js');
firebase.initializeApp({
"apiKey": "API_KEY",
"appId": "APP_ID",
"authDomain": "AUTH_DOMAIN",
"databaseURL": "",
"measurementId": "MEASUREMENT_ID",
"messagingSenderId": "MEASUREMENT_SENDER_ID",
"projectId": "PROJECT_ID",
"storageBucket": "STORAGE_BUCKET"
});

@ -32,6 +32,8 @@
<title>Pinball</title>
<link rel="manifest" href="manifest.json">
<script src="/__/firebase/8.9.1/firebase-app.js"></script>
<script src="/__/firebase/init.js"></script>
</head>
<body>

Loading…
Cancel
Save