66 Commits

Author SHA1 Message Date
gelbeinhalb
0ddb4edbc9 remove unused variable
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
2026-01-21 16:08:21 +01:00
gelbeinhalb
7339194ba0 fix data import and export
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Failing after 2m18s
2026-01-21 15:43:56 +01:00
gelbeinhalb
bd5e38a3ca save game to database on create 2026-01-21 15:42:47 +01:00
gelbeinhalb
12b713bb70 remove skip from test
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2026-01-21 12:04:13 +01:00
gelbeinhalb
e55cea0dcc fix missing await 2026-01-21 12:04:00 +01:00
gelbeinhalb
b2a3a0cf75 fix getting non-existent team throws exception test
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m19s
2026-01-21 11:55:15 +01:00
gelbeinhalb
f142169371 fix overlapping members in teams test 2026-01-21 11:54:00 +01:00
gelbeinhalb
0b778210ef add new team tests
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-21 11:36:23 +01:00
gelbeinhalb
748361d04f add new score tests 2026-01-21 11:33:37 +01:00
gelbeinhalb
2e6a2b8e55 add more player_match tests 2026-01-21 11:26:46 +01:00
gelbeinhalb
15894096f3 fix missing await error 2026-01-21 11:11:46 +01:00
gelbeinhalb
d488eac3ae add more group tests 2026-01-21 11:11:37 +01:00
gelbeinhalb
19b2685714 add more player tests 2026-01-21 11:03:09 +01:00
gelbeinhalb
068ca95afc Merge branch 'feature/88-neue-datenbank-struktur' of git.yannick-weigert.de:liquid-development/game-tracker into feature/88-neue-datenbank-struktur
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m16s
2026-01-20 16:34:43 +01:00
gelbeinhalb
0d28f4b87c remove winner test again 2026-01-20 16:34:38 +01:00
gelbeinhalb
2e454a530a remove winner test again 2026-01-20 16:33:12 +01:00
gelbeinhalb
6c39e1e574 remove winner check
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-20 16:31:52 +01:00
gelbeinhalb
b33260ec23 fix match missing game 2026-01-20 16:31:43 +01:00
gelbeinhalb
b108375ad5 remove winner tests
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m15s
2026-01-20 16:03:37 +01:00
gelbeinhalb
e09ccf9356 add comments to all tests
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-20 15:58:16 +01:00
gelbeinhalb
b0b21bcba6 add new group tests 2026-01-20 15:48:40 +01:00
gelbeinhalb
dec74e9b62 add game tests 2026-01-20 15:41:05 +01:00
gelbeinhalb
4e73babb71 rename game_test to match_test 2026-01-20 15:28:26 +01:00
gelbeinhalb
25e0c75dc6 add team_test.dart.dart
Some checks failed
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
Pull Request Pipeline / test (pull_request) Failing after 1m56s
2026-01-19 19:52:00 +01:00
gelbeinhalb
fb59372c97 add score_test.dart 2026-01-19 19:51:48 +01:00
gelbeinhalb
8dd2f5f8b8 add name to team
Some checks failed
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
Pull Request Pipeline / test (pull_request) Failing after 1m56s
2026-01-19 15:46:25 +01:00
gelbeinhalb
c97fdc2b5f add match method to change created at
Some checks failed
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
Pull Request Pipeline / test (pull_request) Failing after 1m59s
2026-01-19 14:58:33 +01:00
gelbeinhalb
764ce13240 fix felix typos :) 2026-01-19 14:57:19 +01:00
gelbeinhalb
715b3debbb add updateGroupDescription 2026-01-19 14:18:59 +01:00
gelbeinhalb
bb11c86816 Merge remote-tracking branch 'origin/development' into feature/88-neue-datenbank-struktur
# Conflicts:
#	lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart
2026-01-19 14:05:32 +01:00
gelbeinhalb
6a6e36ed7c remove pair
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
2026-01-16 15:55:17 +01:00
gelbeinhalb
d21c37966e tests create testGame now
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2026-01-16 14:15:55 +01:00
gelbeinhalb
b6554c104a Merge remote-tracking branch 'origin/development' into feature/88-neue-datenbank-struktur
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m7s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-16 13:55:05 +01:00
gelbeinhalb
a68bbddc1a temporarily removed group_match_test.dart
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2026-01-16 13:54:15 +01:00
gelbeinhalb
32fa82e5e7 check for game object on match create 2026-01-16 13:53:57 +01:00
gelbeinhalb
49e990dfea add TEMPORARY winner getter and setter methods to match_dao.dart
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 1m57s
Pull Request Pipeline / lint (pull_request) Failing after 3m32s
2026-01-16 13:44:03 +01:00
gelbeinhalb
40e970a5dc re-add winner parameter 2026-01-16 13:37:00 +01:00
gelbeinhalb
b9b6ff85ea update g files
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 2m5s
Pull Request Pipeline / lint (pull_request) Failing after 2m9s
2026-01-16 12:38:25 +01:00
gelbeinhalb
52c1605ce9 update database.dart 2026-01-16 12:38:16 +01:00
gelbeinhalb
c2a9bda8b1 add team_dao.dart 2026-01-16 12:38:09 +01:00
gelbeinhalb
a13d9c55ea add score_dao.dart 2026-01-16 12:37:37 +01:00
gelbeinhalb
b737cae356 remove group_match_dao.dart 2026-01-16 12:27:20 +01:00
gelbeinhalb
3857665444 update player_match_dao.dart 2026-01-16 12:23:24 +01:00
gelbeinhalb
4b900c12bf add description to player 2026-01-16 12:22:23 +01:00
gelbeinhalb
867d0c55da update match_dao.dart
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m8s
Pull Request Pipeline / test (pull_request) Failing after 2m27s
2026-01-16 12:00:07 +01:00
gelbeinhalb
02d3220c1a add group_match_dao.dart 2026-01-16 11:57:50 +01:00
gelbeinhalb
3ca612b0a1 add description attribute to group_dao.dart 2026-01-16 11:56:47 +01:00
gelbeinhalb
cb66b76e5a add game_dao.dart 2026-01-16 11:55:47 +01:00
gelbeinhalb
b9b72cdd50 add pair
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m11s
Pull Request Pipeline / test (pull_request) Failing after 2m30s
2026-01-16 11:10:42 +01:00
gelbeinhalb
4a56df7f8f add team 2026-01-16 11:09:18 +01:00
gelbeinhalb
f2a12265ad add createdAt to team 2026-01-16 11:09:08 +01:00
gelbeinhalb
c82d72544e add description to player 2026-01-16 11:04:54 +01:00
gelbeinhalb
072021bd4c update match class 2026-01-16 10:50:03 +01:00
gelbeinhalb
6a9e5dc9eb add game.dart
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m8s
Pull Request Pipeline / test (pull_request) Failing after 2m29s
2026-01-16 10:28:55 +01:00
gelbeinhalb
be01b5f72a add description to group
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m10s
Pull Request Pipeline / test (pull_request) Failing after 2m29s
2026-01-12 12:51:25 +01:00
gelbeinhalb
e2fe0c7d4d remove group_match_dao
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m18s
Pull Request Pipeline / test (pull_request) Failing after 2m42s
2026-01-12 11:42:03 +01:00
gelbeinhalb
b72ab70e02 add score table
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m22s
Pull Request Pipeline / test (pull_request) Failing after 2m52s
2026-01-12 11:38:38 +01:00
gelbeinhalb
189daf76dd move createdAt below description 2026-01-12 11:35:57 +01:00
gelbeinhalb
0f987f4c7a move match below ids 2026-01-12 11:34:02 +01:00
gelbeinhalb
5dd8f31942 add description to group_table.dart
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m29s
Pull Request Pipeline / test (pull_request) Failing after 2m54s
2026-01-12 11:33:00 +01:00
gelbeinhalb
0394f5edf9 delete group_match_table.dart 2026-01-12 11:32:23 +01:00
gelbeinhalb
d8abad6fd8 add groupid gameid and notes to match 2026-01-12 11:30:37 +01:00
gelbeinhalb
7e6c309de0 add game table 2026-01-12 11:26:39 +01:00
gelbeinhalb
3344575132 add team id and score
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2026-01-12 11:20:40 +01:00
gelbeinhalb
9b66e58dc0 add team table 2026-01-12 11:17:13 +01:00
gelbeinhalb
56562b22bb add description to player 2026-01-12 11:16:02 +01:00
54 changed files with 7611 additions and 1946 deletions

View File

@@ -15,6 +15,43 @@
},
"name": {
"type": "string"
},
"description": {
"type": ["string", "null"]
}
},
"required": [
"id",
"createdAt",
"name"
]
}
},
"games": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"name": {
"type": "string"
},
"ruleset": {
"type": ["string", "null"]
},
"description": {
"type": ["string", "null"]
},
"color": {
"type": ["integer", "null"]
},
"icon": {
"type": ["string", "null"]
}
},
"required": [
@@ -25,6 +62,38 @@
}
},
"groups": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"description": {
"type": ["string", "null"]
},
"memberIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"id",
"name",
"createdAt",
"memberIds"
]
}
},
"teams": {
"type": "array",
"items": {
"type": "object",
@@ -67,6 +136,12 @@
"createdAt": {
"type": "string"
},
"gameId": {
"anyOf": [
{"type": "string"},
{"type": "null"}
]
},
"groupId": {
"anyOf": [
{"type": "string"},
@@ -79,7 +154,7 @@
"type": "string"
}
},
"winnerId": {
"notes": {
"anyOf": [
{"type": "string"},
{"type": "null"}
@@ -90,7 +165,6 @@
"id",
"name",
"createdAt",
"groupId",
"playerIds"
]
}
@@ -98,7 +172,9 @@
},
"required": [
"players",
"games",
"groups",
"teams",
"matches"
]
}

View File

@@ -19,7 +19,4 @@ class Constants {
/// Maximum length for team names
static const int MAX_TEAM_NAME_LENGTH = 32;
/// Maximum length for game descriptions
static const int MAX_GAME_DESCRIPTION_LENGTH = 256;
}

179
lib/data/dao/game_dao.dart Normal file
View File

@@ -0,0 +1,179 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/dto/game.dart';
part 'game_dao.g.dart';
@DriftAccessor(tables: [GameTable])
class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
GameDao(super.db);
/// Retrieves all games from the database.
Future<List<Game>> getAllGames() async {
final query = select(gameTable);
final result = await query.get();
return result
.map(
(row) => Game(
id: row.id,
name: row.name,
ruleset: row.ruleset,
description: row.description,
color: row.color != null ? int.tryParse(row.color!) : null,
icon: row.icon,
createdAt: row.createdAt,
),
)
.toList();
}
/// Retrieves a [Game] by its [gameId].
Future<Game> getGameById({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingle();
return Game(
id: result.id,
name: result.name,
ruleset: result.ruleset,
description: result.description,
color: result.color != null ? int.tryParse(result.color!) : null,
icon: result.icon,
createdAt: result.createdAt,
);
}
/// Adds a new [game] to the database.
/// If a game with the same ID already exists, no action is taken.
/// Returns `true` if the game was added, `false` otherwise.
Future<bool> addGame({required Game game}) async {
if (!await gameExists(gameId: game.id)) {
await into(gameTable).insert(
GameTableCompanion.insert(
id: game.id,
name: game.name,
ruleset: game.ruleset ?? '',
description: Value(game.description),
color: Value(game.color?.toString()),
icon: Value(game.icon),
createdAt: game.createdAt,
),
mode: InsertMode.insertOrReplace,
);
return true;
}
return false;
}
/// Adds multiple [games] to the database in a batch operation.
/// Uses insertOrIgnore to avoid overwriting existing games.
Future<bool> addGamesAsList({required List<Game> games}) async {
if (games.isEmpty) return false;
await db.batch(
(b) => b.insertAll(
gameTable,
games
.map(
(game) => GameTableCompanion.insert(
id: game.id,
name: game.name,
ruleset: game.ruleset ?? '',
description: Value(game.description),
color: Value(game.color?.toString()),
icon: Value(game.icon),
createdAt: game.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrIgnore,
),
);
return true;
}
/// Deletes the game with the given [gameId] from the database.
/// Returns `true` if the game was deleted, `false` if the game did not exist.
Future<bool> deleteGame({required String gameId}) async {
final query = delete(gameTable)..where((g) => g.id.equals(gameId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Checks if a game with the given [gameId] exists in the database.
/// Returns `true` if the game exists, `false` otherwise.
Future<bool> gameExists({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingleOrNull();
return result != null;
}
/// Updates the name of the game with the given [gameId] to [newName].
Future<void> updateGameName({
required String gameId,
required String newName,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(name: Value(newName)),
);
}
/// Updates the ruleset of the game with the given [gameId].
Future<void> updateGameRuleset({
required String gameId,
required String newRuleset,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(ruleset: Value(newRuleset)),
);
}
/// Updates the description of the game with the given [gameId].
Future<void> updateGameDescription({
required String gameId,
required String? newDescription,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(description: Value(newDescription)),
);
}
/// Updates the color of the game with the given [gameId].
Future<void> updateGameColor({
required String gameId,
required int? newColor,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(color: Value(newColor?.toString())),
);
}
/// Updates the icon of the game with the given [gameId].
Future<void> updateGameIcon({
required String gameId,
required String? newIcon,
}) async {
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
GameTableCompanion(icon: Value(newIcon)),
);
}
/// Retrieves the total count of games in the database.
Future<int> getGameCount() async {
final count =
await (selectOnly(gameTable)..addColumns([gameTable.id.count()]))
.map((row) => row.read(gameTable.id.count()))
.getSingle();
return count ?? 0;
}
/// Deletes all games from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteAllGames() async {
final query = delete(gameTable);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
}

View File

@@ -0,0 +1,8 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'game_dao.dart';
// ignore_for_file: type=lint
mixin _$GameDaoMixin on DatabaseAccessor<AppDatabase> {
$GameTableTable get gameTable => attachedDatabase.gameTable;
}

View File

@@ -23,6 +23,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
return Group(
id: groupData.id,
name: groupData.name,
description: groupData.description,
members: members,
createdAt: groupData.createdAt,
);
@@ -42,6 +43,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
return Group(
id: result.id,
name: result.name,
description: result.description,
members: members,
createdAt: result.createdAt,
);
@@ -56,6 +58,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
GroupTableCompanion.insert(
id: group.id,
name: group.name,
description: Value(group.description),
createdAt: group.createdAt,
),
mode: InsertMode.insertOrReplace,
@@ -105,6 +108,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
(group) => GroupTableCompanion.insert(
id: group.id,
name: group.name,
description: Value(group.description),
createdAt: group.createdAt,
),
)
@@ -132,6 +136,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
(p) => PlayerTableCompanion.insert(
id: p.id,
name: p.name,
description: Value(p.description),
createdAt: p.createdAt,
),
)
@@ -176,7 +181,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
/// Updates the name of the group with the given [id] to [newName].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGroupname({
Future<bool> updateGroupName({
required String groupId,
required String newName,
}) async {
@@ -187,6 +192,21 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
return rowsAffected > 0;
}
/// Updates the description of the group with the given [groupId] to [newDescription].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGroupDescription({
required String groupId,
required String? newDescription,
}) async {
final rowsAffected =
await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
GroupTableCompanion(description: Value(newDescription)),
);
return rowsAffected > 0;
}
/// Retrieves the number of groups in the database.
Future<int> getGroupCount() async {
final count =

View File

@@ -1,98 +0,0 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/group_match_table.dart';
import 'package:game_tracker/data/dto/group.dart';
part 'group_match_dao.g.dart';
@DriftAccessor(tables: [GroupMatchTable])
class GroupMatchDao extends DatabaseAccessor<AppDatabase>
with _$GroupMatchDaoMixin {
GroupMatchDao(super.db);
/// Associates a group with a match by inserting a record into the
/// [GroupMatchTable].
Future<void> addGroupToMatch({
required String matchId,
required String groupId,
}) async {
if (await matchHasGroup(matchId: matchId)) {
throw Exception('Match already has a group');
}
await into(groupMatchTable).insert(
GroupMatchTableCompanion.insert(groupId: groupId, matchId: matchId),
mode: InsertMode.insertOrIgnore,
);
}
/// Retrieves the [Group] associated with the given [matchId].
/// Returns `null` if no group is found.
Future<Group?> getGroupOfMatch({required String matchId}) async {
final result = await (select(
groupMatchTable,
)..where((g) => g.matchId.equals(matchId))).getSingleOrNull();
if (result == null) {
return null;
}
final group = await db.groupDao.getGroupById(groupId: result.groupId);
return group;
}
/// Checks if there is a group associated with the given [matchId].
/// Returns `true` if there is a group, otherwise `false`.
Future<bool> matchHasGroup({required String matchId}) async {
final count =
await (selectOnly(groupMatchTable)
..where(groupMatchTable.matchId.equals(matchId))
..addColumns([groupMatchTable.groupId.count()]))
.map((row) => row.read(groupMatchTable.groupId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Checks if a specific group is associated with a specific match.
/// Returns `true` if the group is in the match, otherwise `false`.
Future<bool> isGroupInMatch({
required String matchId,
required String groupId,
}) async {
final count =
await (selectOnly(groupMatchTable)
..where(
groupMatchTable.matchId.equals(matchId) &
groupMatchTable.groupId.equals(groupId),
)
..addColumns([groupMatchTable.groupId.count()]))
.map((row) => row.read(groupMatchTable.groupId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Removes the association of a group from a match based on [groupId] and
/// [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeGroupFromMatch({
required String matchId,
required String groupId,
}) async {
final query = delete(groupMatchTable)
..where((g) => g.matchId.equals(matchId) & g.groupId.equals(groupId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Updates the group associated with a match to [newGroupId] based on
/// [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGroupOfMatch({
required String matchId,
required String newGroupId,
}) async {
final updatedRows =
await (update(groupMatchTable)..where((g) => g.matchId.equals(matchId)))
.write(GroupMatchTableCompanion(groupId: Value(newGroupId)));
return updatedRows > 0;
}
}

View File

@@ -1,10 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'group_match_dao.dart';
// ignore_for_file: type=lint
mixin _$GroupMatchDaoMixin on DatabaseAccessor<AppDatabase> {
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$GroupMatchTableTable get groupMatchTable => attachedDatabase.groupMatchTable;
}

View File

@@ -1,13 +1,17 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_match_table.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
part 'match_dao.g.dart';
@DriftAccessor(tables: [MatchTable])
@DriftAccessor(tables: [MatchTable, GameTable, GroupTable, PlayerMatchTable])
class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
MatchDao(super.db);
@@ -18,20 +22,22 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return Future.wait(
result.map((row) async {
final group = await db.groupMatchDao.getGroupOfMatch(matchId: row.id);
final game = await db.gameDao.getGameById(gameId: row.gameId);
Group? group;
if (row.groupId != null) {
group = await db.groupDao.getGroupById(groupId: row.groupId!);
}
final players = await db.playerMatchDao.getPlayersOfMatch(
matchId: row.id,
);
final winner = row.winnerId != null
? await db.playerDao.getPlayerById(playerId: row.winnerId!)
: null;
return Match(
id: row.id,
name: row.name,
name: row.name ?? '',
game: game,
group: group,
players: players,
notes: row.notes,
createdAt: row.createdAt,
winner: winner,
);
}),
);
@@ -42,100 +48,110 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final query = select(matchTable)..where((g) => g.id.equals(matchId));
final result = await query.getSingle();
final game = await db.gameDao.getGameById(gameId: result.gameId);
Group? group;
if (result.groupId != null) {
group = await db.groupDao.getGroupById(groupId: result.groupId!);
}
List<Player>? players;
if (await db.playerMatchDao.matchHasPlayers(matchId: matchId)) {
players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
}
Group? group;
if (await db.groupMatchDao.matchHasGroup(matchId: matchId)) {
group = await db.groupMatchDao.getGroupOfMatch(matchId: matchId);
}
Player? winner;
if (result.winnerId != null) {
winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
}
return Match(
id: result.id,
name: result.name,
players: players,
name: result.name ?? '',
game: game,
group: group,
winner: winner,
players: players,
notes: result.notes,
createdAt: result.createdAt,
);
}
/// Adds a new [Match] to the database. Also adds players and group
/// associations. This method assumes that the players and groups added to
/// this match are already present in the database.
/// Adds a new [Match] to the database. Also adds players associations.
/// This method assumes that the game and group (if any) are already present
/// in the database.
Future<void> addMatch({required Match match}) async {
if (match.game == null) {
throw ArgumentError('Match must have a game associated with it');
}
await db.transaction(() async {
await into(matchTable).insert(
MatchTableCompanion.insert(
id: match.id,
name: match.name,
winnerId: Value(match.winner?.id),
gameId: match.game!.id,
groupId: Value(match.group?.id),
name: Value(match.name),
notes: Value(match.notes),
createdAt: match.createdAt,
),
mode: InsertMode.insertOrReplace,
);
if (match.players != null) {
for (final p in match.players ?? []) {
for (final p in match.players!) {
await db.playerMatchDao.addPlayerToMatch(
matchId: match.id,
playerId: p.id,
);
}
}
if (match.group != null) {
await db.groupMatchDao.addGroupToMatch(
matchId: match.id,
groupId: match.group!.id,
);
}
});
}
/// Adds multiple [Match]s to the database in a batch operation.
/// Adds multiple [Match]es to the database in a batch operation.
/// Also adds associated players and groups if they exist.
/// If the [matches] list is empty, the method returns immediately.
/// This Method should only be used to import matches from a different device.
/// This method should only be used to import matches from a different device.
Future<void> addMatchAsList({required List<Match> matches}) async {
if (matches.isEmpty) return;
await db.transaction(() async {
// Add all matches in batch
await db.batch(
(b) => b.insertAll(
matchTable,
matches
.map(
(match) => MatchTableCompanion.insert(
id: match.id,
name: match.name,
createdAt: match.createdAt,
winnerId: Value(match.winner?.id),
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
// Add all games first (deduplicated)
final uniqueGames = <String, Game>{};
for (final match in matches) {
if (match.game != null) {
uniqueGames[match.game!.id] = match.game!;
}
}
if (uniqueGames.isNotEmpty) {
await db.batch(
(b) => b.insertAll(
db.gameTable,
uniqueGames.values
.map(
(game) => GameTableCompanion.insert(
id: game.id,
name: game.name,
ruleset: game.ruleset ?? '',
description: Value(game.description),
color: Value(game.color?.toString()),
icon: Value(game.icon),
createdAt: game.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrIgnore,
),
);
}
// Add all groups of the matches in batch
// Using insertOrIgnore to avoid overwriting existing groups (which would
// trigger cascade deletes on player_group associations)
await db.batch(
(b) => b.insertAll(
db.groupTable,
matches
.where((match) => match.group != null)
.map(
(matches) => GroupTableCompanion.insert(
id: matches.group!.id,
name: matches.group!.name,
createdAt: matches.group!.createdAt,
(match) => GroupTableCompanion.insert(
id: match.group!.id,
name: match.group!.name,
description: Value(match.group!.description),
createdAt: match.group!.createdAt,
),
)
.toList(),
@@ -143,6 +159,27 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
),
);
// Add all matches in batch
await db.batch(
(b) => b.insertAll(
matchTable,
matches
.where((match) => match.game != null)
.map(
(match) => MatchTableCompanion.insert(
id: match.id,
gameId: match.game!.id,
groupId: Value(match.group?.id),
name: Value(match.name),
notes: Value(match.notes),
createdAt: match.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrReplace,
),
);
// Add all players of the matches in batch (unique)
final uniquePlayers = <String, Player>{};
for (final match in matches) {
@@ -160,8 +197,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
}
if (uniquePlayers.isNotEmpty) {
// Using insertOrIgnore to avoid triggering cascade deletes on
// player_group/player_match associations when players already exist
await db.batch(
(b) => b.insertAll(
db.playerTable,
@@ -170,6 +205,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
(p) => PlayerTableCompanion.insert(
id: p.id,
name: p.name,
description: Value(p.description),
createdAt: p.createdAt,
),
)
@@ -183,12 +219,13 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
await db.batch((b) {
for (final match in matches) {
if (match.players != null) {
for (final p in match.players ?? []) {
for (final p in match.players!) {
b.insert(
db.playerMatchTable,
PlayerMatchTableCompanion.insert(
matchId: match.id,
playerId: p.id,
score: 0,
),
mode: InsertMode.insertOrIgnore,
);
@@ -214,22 +251,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
}
}
});
// Add all group-match associations in batch
await db.batch((b) {
for (final match in matches) {
if (match.group != null) {
b.insert(
db.groupMatchTable,
GroupMatchTableCompanion.insert(
matchId: match.id,
groupId: match.group!.id,
),
mode: InsertMode.insertOrIgnore,
);
}
}
});
});
}
@@ -266,52 +287,20 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return rowsAffected > 0;
}
/// Sets the winner of the match with the given [matchId] to the player with
/// the given [winnerId].
/// Updates the notes of the match with the given [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> setWinner({
Future<bool> updateMatchNotes({
required String matchId,
required String winnerId,
required String? notes,
}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
MatchTableCompanion(winnerId: Value(winnerId)),
MatchTableCompanion(notes: Value(notes)),
);
return rowsAffected > 0;
}
/// Retrieves the winner of the match with the given [matchId].
/// Returns the [Player] who won the match, or `null` if no winner is set.
Future<Player?> getWinner({required String matchId}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId));
final result = await query.getSingleOrNull();
if (result == null || result.winnerId == null) {
return null;
}
final winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
return winner;
}
/// Removes the winner of the match with the given [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeWinner({required String matchId}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
const MatchTableCompanion(winnerId: Value(null)),
);
return rowsAffected > 0;
}
/// Checks if the match with the given [matchId] has a winner set.
/// Returns `true` if a winner is set, otherwise `false`.
Future<bool> hasWinner({required String matchId}) async {
final query = select(matchTable)
..where((g) => g.id.equals(matchId) & g.winnerId.isNotNull());
final result = await query.getSingleOrNull();
return result != null;
}
/// Changes the title of the match with the given [matchId] to [newName].
/// Changes the name of the match with the given [matchId] to [newName].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchName({
required String matchId,
@@ -323,4 +312,80 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
);
return rowsAffected > 0;
}
/// Updates the game of the match with the given [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchGame({
required String matchId,
required String gameId,
}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
MatchTableCompanion(gameId: Value(gameId)),
);
return rowsAffected > 0;
}
/// Updates the group of the match with the given [matchId].
/// Pass null to remove the group association.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchGroup({
required String matchId,
required String? groupId,
}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
MatchTableCompanion(groupId: Value(groupId)),
);
return rowsAffected > 0;
}
/// Updates the createdAt timestamp of the match with the given [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateMatchCreatedAt({
required String matchId,
required DateTime createdAt,
}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId));
final rowsAffected = await query.write(
MatchTableCompanion(createdAt: Value(createdAt)),
);
return rowsAffected > 0;
}
// ============================================================
// TEMPORARY: Winner methods - these are stubs and do not persist data
// TODO: Implement proper winner handling
// ============================================================
/// TEMPORARY: Checks if a match has a winner.
/// Currently returns true if the match has any players.
Future<bool> hasWinner({required String matchId}) async {
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
return players?.isNotEmpty ?? false;
}
/// TEMPORARY: Gets the winner of a match.
/// Currently returns the first player in the match's player list.
Future<Player?> getWinner({required String matchId}) async {
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
return (players?.isNotEmpty ?? false) ? players!.first : null;
}
/// TEMPORARY: Sets the winner of a match.
/// Currently does nothing - winner is not persisted.
Future<bool> setWinner({
required String matchId,
required String winnerId,
}) async {
// TODO: Implement winner persistence
return true;
}
/// TEMPORARY: Removes the winner of a match.
/// Currently does nothing - winner is not persisted.
Future<bool> removeWinner({required String matchId}) async {
// TODO: Implement winner persistence
return true;
}
}

View File

@@ -4,5 +4,11 @@ part of 'match_dao.dart';
// ignore_for_file: type=lint
mixin _$MatchDaoMixin on DatabaseAccessor<AppDatabase> {
$GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$TeamTableTable get teamTable => attachedDatabase.teamTable;
$PlayerMatchTableTable get playerMatchTable =>
attachedDatabase.playerMatchTable;
}

View File

@@ -15,7 +15,12 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
final result = await query.get();
return result
.map(
(row) => Player(id: row.id, name: row.name, createdAt: row.createdAt),
(row) => Player(
id: row.id,
name: row.name,
description: row.description,
createdAt: row.createdAt,
),
)
.toList();
}
@@ -27,6 +32,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
return Player(
id: result.id,
name: result.name,
description: result.description,
createdAt: result.createdAt,
);
}
@@ -40,6 +46,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
PlayerTableCompanion.insert(
id: player.id,
name: player.name,
description: Value(player.description),
createdAt: player.createdAt,
),
mode: InsertMode.insertOrReplace,
@@ -63,6 +70,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
(player) => PlayerTableCompanion.insert(
id: player.id,
name: player.name,
description: Value(player.description),
createdAt: player.createdAt,
),
)
@@ -91,7 +99,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
}
/// Updates the name of the player with the given [playerId] to [newName].
Future<void> updatePlayername({
Future<void> updatePlayerName({
required String playerId,
required String newName,
}) async {

View File

@@ -27,7 +27,7 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
}
if (!await db.playerDao.playerExists(playerId: player.id)) {
db.playerDao.addPlayer(player: player);
await db.playerDao.addPlayer(player: player);
}
await into(playerGroupTable).insert(

View File

@@ -1,23 +1,31 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/player_match_table.dart';
import 'package:game_tracker/data/db/tables/team_table.dart';
import 'package:game_tracker/data/dto/player.dart';
part 'player_match_dao.g.dart';
@DriftAccessor(tables: [PlayerMatchTable])
@DriftAccessor(tables: [PlayerMatchTable, TeamTable])
class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
with _$PlayerMatchDaoMixin {
PlayerMatchDao(super.db);
/// Associates a player with a match by inserting a record into the
/// [PlayerMatchTable].
/// [PlayerMatchTable]. Optionally associates with a team and sets initial score.
Future<void> addPlayerToMatch({
required String matchId,
required String playerId,
String? teamId,
int score = 0,
}) async {
await into(playerMatchTable).insert(
PlayerMatchTableCompanion.insert(playerId: playerId, matchId: matchId),
PlayerMatchTableCompanion.insert(
playerId: playerId,
matchId: matchId,
teamId: Value(teamId),
score: score,
),
mode: InsertMode.insertOrIgnore,
);
}
@@ -38,6 +46,50 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
return players;
}
/// Retrieves a player's score for a specific match.
/// Returns null if the player is not in the match.
Future<int?> getPlayerScore({
required String matchId,
required String playerId,
}) async {
final result = await (select(playerMatchTable)
..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
))
.getSingleOrNull();
return result?.score;
}
/// Updates the score for a player in a match.
/// Returns `true` if the update was successful, otherwise `false`.
Future<bool> updatePlayerScore({
required String matchId,
required String playerId,
required int newScore,
}) async {
final rowsAffected = await (update(playerMatchTable)
..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
))
.write(PlayerMatchTableCompanion(score: Value(newScore)));
return rowsAffected > 0;
}
/// Updates the team for a player in a match.
/// Returns `true` if the update was successful, otherwise `false`.
Future<bool> updatePlayerTeam({
required String matchId,
required String playerId,
required String? teamId,
}) async {
final rowsAffected = await (update(playerMatchTable)
..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
))
.write(PlayerMatchTableCompanion(teamId: Value(teamId)));
return rowsAffected > 0;
}
/// Checks if there are any players associated with the given [matchId].
/// Returns `true` if there are players, otherwise `false`.
Future<bool> matchHasPlayers({required String matchId}) async {
@@ -96,7 +148,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds);
final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet);
db.transaction(() async {
await db.transaction(() async {
// Remove old players
if (playersToRemove.isNotEmpty) {
await (delete(playerMatchTable)..where(
@@ -114,6 +166,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
(id) => PlayerMatchTableCompanion.insert(
playerId: id,
matchId: matchId,
score: 0,
),
)
.toList();
@@ -127,4 +180,23 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
}
});
}
/// Retrieves all players in a specific team for a match.
Future<List<Player>> getPlayersInTeam({
required String matchId,
required String teamId,
}) async {
final result = await (select(playerMatchTable)
..where(
(p) => p.matchId.equals(matchId) & p.teamId.equals(teamId),
))
.get();
if (result.isEmpty) return [];
final futures = result.map(
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
);
return Future.wait(futures);
}
}

View File

@@ -5,7 +5,10 @@ part of 'player_match_dao.dart';
// ignore_for_file: type=lint
mixin _$PlayerMatchDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$TeamTableTable get teamTable => attachedDatabase.teamTable;
$PlayerMatchTableTable get playerMatchTable =>
attachedDatabase.playerMatchTable;
}

191
lib/data/dao/score_dao.dart Normal file
View File

@@ -0,0 +1,191 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/score_table.dart';
part 'score_dao.g.dart';
/// A data class representing a score entry.
class ScoreEntry {
final String playerId;
final String matchId;
final int roundNumber;
final int score;
final int change;
ScoreEntry({
required this.playerId,
required this.matchId,
required this.roundNumber,
required this.score,
required this.change,
});
}
@DriftAccessor(tables: [ScoreTable])
class ScoreDao extends DatabaseAccessor<AppDatabase> with _$ScoreDaoMixin {
ScoreDao(super.db);
/// Adds a score entry to the database.
Future<void> addScore({
required String playerId,
required String matchId,
required int roundNumber,
required int score,
required int change,
}) async {
await into(scoreTable).insert(
ScoreTableCompanion.insert(
playerId: playerId,
matchId: matchId,
roundNumber: roundNumber,
score: score,
change: change,
),
mode: InsertMode.insertOrReplace,
);
}
/// Retrieves all scores for a specific match.
Future<List<ScoreEntry>> getScoresForMatch({required String matchId}) async {
final query = select(scoreTable)..where((s) => s.matchId.equals(matchId));
final result = await query.get();
return result
.map(
(row) => ScoreEntry(
playerId: row.playerId,
matchId: row.matchId,
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
),
)
.toList();
}
/// Retrieves all scores for a specific player in a match.
Future<List<ScoreEntry>> getPlayerScoresInMatch({
required String playerId,
required String matchId,
}) async {
final query = select(scoreTable)
..where(
(s) => s.playerId.equals(playerId) & s.matchId.equals(matchId),
)
..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]);
final result = await query.get();
return result
.map(
(row) => ScoreEntry(
playerId: row.playerId,
matchId: row.matchId,
roundNumber: row.roundNumber,
score: row.score,
change: row.change,
),
)
.toList();
}
/// Retrieves the score for a specific round.
Future<ScoreEntry?> getScoreForRound({
required String playerId,
required String matchId,
required int roundNumber,
}) async {
final query = select(scoreTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
);
final result = await query.getSingleOrNull();
if (result == null) return null;
return ScoreEntry(
playerId: result.playerId,
matchId: result.matchId,
roundNumber: result.roundNumber,
score: result.score,
change: result.change,
);
}
/// Updates a score entry.
Future<bool> updateScore({
required String playerId,
required String matchId,
required int roundNumber,
required int newScore,
required int newChange,
}) async {
final rowsAffected = await (update(scoreTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
))
.write(
ScoreTableCompanion(
score: Value(newScore),
change: Value(newChange),
),
);
return rowsAffected > 0;
}
/// Deletes a score entry.
Future<bool> deleteScore({
required String playerId,
required String matchId,
required int roundNumber,
}) async {
final query = delete(scoreTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Deletes all scores for a specific match.
Future<bool> deleteScoresForMatch({required String matchId}) async {
final query = delete(scoreTable)..where((s) => s.matchId.equals(matchId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Deletes all scores for a specific player.
Future<bool> deleteScoresForPlayer({required String playerId}) async {
final query = delete(scoreTable)..where((s) => s.playerId.equals(playerId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Gets the latest round number for a match.
Future<int> getLatestRoundNumber({required String matchId}) async {
final query = selectOnly(scoreTable)
..where(scoreTable.matchId.equals(matchId))
..addColumns([scoreTable.roundNumber.max()]);
final result = await query.getSingle();
return result.read(scoreTable.roundNumber.max()) ?? 0;
}
/// Gets the total score for a player in a match (sum of all changes).
Future<int> getTotalScoreForPlayer({
required String playerId,
required String matchId,
}) async {
final scores = await getPlayerScoresInMatch(
playerId: playerId,
matchId: matchId,
);
if (scores.isEmpty) return 0;
// Return the score from the latest round
return scores.last.score;
}
}

View File

@@ -0,0 +1,12 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'score_dao.dart';
// ignore_for_file: type=lint
mixin _$ScoreDaoMixin on DatabaseAccessor<AppDatabase> {
$PlayerTableTable get playerTable => attachedDatabase.playerTable;
$GameTableTable get gameTable => attachedDatabase.gameTable;
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$ScoreTableTable get scoreTable => attachedDatabase.scoreTable;
}

147
lib/data/dao/team_dao.dart Normal file
View File

@@ -0,0 +1,147 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/team_table.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/data/dto/team.dart';
part 'team_dao.g.dart';
@DriftAccessor(tables: [TeamTable])
class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
TeamDao(super.db);
/// Retrieves all teams from the database.
/// Note: This returns teams without their members. Use getTeamById for full team data.
Future<List<Team>> getAllTeams() async {
final query = select(teamTable);
final result = await query.get();
return Future.wait(
result.map((row) async {
final members = await _getTeamMembers(teamId: row.id);
return Team(
id: row.id,
name: row.name,
createdAt: row.createdAt,
members: members,
);
}),
);
}
/// Retrieves a [Team] by its [teamId], including its members.
Future<Team> getTeamById({required String teamId}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId));
final result = await query.getSingle();
final members = await _getTeamMembers(teamId: teamId);
return Team(
id: result.id,
name: result.name,
createdAt: result.createdAt,
members: members,
);
}
/// Helper method to get team members from player_match_table.
/// This assumes team members are tracked via the player_match_table.
Future<List<Player>> _getTeamMembers({required String teamId}) async {
// Get all player_match entries with this teamId
final playerMatchQuery = select(db.playerMatchTable)
..where((pm) => pm.teamId.equals(teamId));
final playerMatches = await playerMatchQuery.get();
if (playerMatches.isEmpty) return [];
// Get unique player IDs
final playerIds = playerMatches.map((pm) => pm.playerId).toSet();
// Fetch all players
final players = await Future.wait(
playerIds.map((id) => db.playerDao.getPlayerById(playerId: id)),
);
return players;
}
/// Adds a new [team] to the database.
/// Returns `true` if the team was added, `false` otherwise.
Future<bool> addTeam({required Team team}) async {
if (!await teamExists(teamId: team.id)) {
await into(teamTable).insert(
TeamTableCompanion.insert(
id: team.id,
name: team.name,
createdAt: team.createdAt,
),
mode: InsertMode.insertOrReplace,
);
return true;
}
return false;
}
/// Adds multiple [teams] to the database in a batch operation.
Future<bool> addTeamsAsList({required List<Team> teams}) async {
if (teams.isEmpty) return false;
await db.batch(
(b) => b.insertAll(
teamTable,
teams
.map(
(team) => TeamTableCompanion.insert(
id: team.id,
name: team.name,
createdAt: team.createdAt,
),
)
.toList(),
mode: InsertMode.insertOrIgnore,
),
);
return true;
}
/// Deletes the team with the given [teamId] from the database.
/// Returns `true` if the team was deleted, `false` otherwise.
Future<bool> deleteTeam({required String teamId}) async {
final query = delete(teamTable)..where((t) => t.id.equals(teamId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Checks if a team with the given [teamId] exists in the database.
/// Returns `true` if the team exists, `false` otherwise.
Future<bool> teamExists({required String teamId}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId));
final result = await query.getSingleOrNull();
return result != null;
}
/// Updates the name of the team with the given [teamId].
Future<void> updateTeamName({
required String teamId,
required String newName,
}) async {
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
TeamTableCompanion(name: Value(newName)),
);
}
/// Retrieves the total count of teams in the database.
Future<int> getTeamCount() async {
final count =
await (selectOnly(teamTable)..addColumns([teamTable.id.count()]))
.map((row) => row.read(teamTable.id.count()))
.getSingle();
return count ?? 0;
}
/// Deletes all teams from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteAllTeams() async {
final query = delete(teamTable);
final rowsAffected = await query.go();
return rowsAffected > 0;
}
}

View File

@@ -0,0 +1,8 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'team_dao.dart';
// ignore_for_file: type=lint
mixin _$TeamDaoMixin on DatabaseAccessor<AppDatabase> {
$TeamTableTable get teamTable => attachedDatabase.teamTable;
}

View File

@@ -1,17 +1,21 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:game_tracker/data/dao/game_dao.dart';
import 'package:game_tracker/data/dao/group_dao.dart';
import 'package:game_tracker/data/dao/group_match_dao.dart';
import 'package:game_tracker/data/dao/match_dao.dart';
import 'package:game_tracker/data/dao/player_dao.dart';
import 'package:game_tracker/data/dao/player_group_dao.dart';
import 'package:game_tracker/data/dao/player_match_dao.dart';
import 'package:game_tracker/data/db/tables/group_match_table.dart';
import 'package:game_tracker/data/dao/score_dao.dart';
import 'package:game_tracker/data/dao/team_dao.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_group_table.dart';
import 'package:game_tracker/data/db/tables/player_match_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
import 'package:game_tracker/data/db/tables/score_table.dart';
import 'package:game_tracker/data/db/tables/team_table.dart';
import 'package:path_provider/path_provider.dart';
part 'database.g.dart';
@@ -20,25 +24,29 @@ part 'database.g.dart';
tables: [
PlayerTable,
GroupTable,
GameTable,
TeamTable,
MatchTable,
PlayerGroupTable,
PlayerMatchTable,
GroupMatchTable,
ScoreTable,
],
daos: [
PlayerDao,
GroupDao,
GameDao,
TeamDao,
MatchDao,
PlayerGroupDao,
PlayerMatchDao,
GroupMatchDao,
ScoreDao,
],
)
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
int get schemaVersion => 2;
@override
MigrationStrategy get migration {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
import 'package:drift/drift.dart';
class GameTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get ruleset => text()();
TextColumn get description => text().nullable()();
TextColumn get color => text().nullable()();
TextColumn get icon => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column<Object>> get primaryKey => {id};
}

View File

@@ -1,13 +0,0 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
class GroupMatchTable extends Table {
TextColumn get groupId =>
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
@override
Set<Column<Object>> get primaryKey => {groupId, matchId};
}

View File

@@ -3,6 +3,7 @@ import 'package:drift/drift.dart';
class GroupTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get description => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override

View File

@@ -1,9 +1,15 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
class MatchTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
late final winnerId = text().nullable()();
TextColumn get gameId =>
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
TextColumn get groupId =>
text().references(GroupTable, #id, onDelete: KeyAction.cascade).nullable()(); // Nullable if not part of a group
TextColumn get name => text().nullable()();
TextColumn get notes => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override

View File

@@ -1,12 +1,16 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
import 'package:game_tracker/data/db/tables/team_table.dart';
class PlayerMatchTable extends Table {
TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
TextColumn get teamId =>
text().references(TeamTable, #id).nullable()();
IntColumn get score => integer()();
@override
Set<Column<Object>> get primaryKey => {playerId, matchId};

View File

@@ -3,6 +3,7 @@ import 'package:drift/drift.dart';
class PlayerTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get description => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override

View File

@@ -0,0 +1,16 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
class ScoreTable extends Table {
TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
IntColumn get roundNumber => integer()();
IntColumn get score => integer()();
IntColumn get change => integer()();
@override
Set<Column<Object>> get primaryKey => {playerId, matchId, roundNumber};
}

View File

@@ -0,0 +1,10 @@
import 'package:drift/drift.dart';
class TeamTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column<Object>> get primaryKey => {id};
}

View File

@@ -18,6 +18,33 @@ class Game {
this.description,
this.color,
this.icon,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
String toString() {
return 'Game{id: $id, name: $name, ruleset: $ruleset, description: $description, color: $color, icon: $icon}';
}
/// Creates a Game instance from a JSON object.
Game.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
ruleset = json['ruleset'],
description = json['description'],
color = json['color'],
icon = json['icon'];
/// Converts the Game instance to a JSON object.
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'ruleset': ruleset,
'description': description,
'color': color,
'icon': icon,
};
}

View File

@@ -4,37 +4,40 @@ import 'package:uuid/uuid.dart';
class Group {
final String id;
final DateTime createdAt;
final String name;
final String? description;
final DateTime createdAt;
final List<Player> members;
Group({
String? id,
DateTime? createdAt,
required this.name,
this.description,
required this.members,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
String toString() {
return 'Group{id: $id, name: $name,members: $members}';
return 'Group{id: $id, name: $name, description: $description, members: $members}';
}
/// Creates a Group instance from a JSON object.
/// Creates a Group instance from a JSON object (memberIds format).
/// Player objects are reconstructed from memberIds by the DataTransferService.
Group.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'],
members = (json['members'] as List)
.map((memberJson) => Player.fromJson(memberJson))
.toList();
description = json['description'],
members = []; // Populated during import via DataTransferService
/// Converts the Group instance to a JSON object.
/// Converts the Group instance to a JSON object using normalized format (memberIds only).
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'members': members.map((member) => member.toJson()).toList(),
'description': description,
'memberIds': members.map((member) => member.id).toList(),
};
}

View File

@@ -1,4 +1,5 @@
import 'package:clock/clock.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:uuid/uuid.dart';
@@ -7,45 +8,48 @@ class Match {
final String id;
final DateTime createdAt;
final String name;
final List<Player>? players;
final Game? game;
final Group? group;
final List<Player>? players;
final String? notes;
Player? winner;
Match({
String? id,
DateTime? createdAt,
required this.name,
this.players,
this.game,
this.group,
this.players,
this.notes,
this.winner,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
String toString() {
return 'Match{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}';
return 'Match{id: $id, name: $name, game: $game, group: $group, players: $players, notes: $notes}';
}
/// Creates a Match instance from a JSON object.
/// Creates a Match instance from a JSON object (ID references format).
/// Related objects are reconstructed from IDs by the DataTransferService.
Match.fromJson(Map<String, dynamic> json)
: id = json['id'],
name = json['name'],
createdAt = DateTime.parse(json['createdAt']),
players = json['players'] != null
? (json['players'] as List)
.map((playerJson) => Player.fromJson(playerJson))
.toList()
: null,
group = json['group'] != null ? Group.fromJson(json['group']) : null,
winner = json['winner'] != null ? Player.fromJson(json['winner']) : null;
name = json['name'],
game = null, // Populated during import via DataTransferService
group = null, // Populated during import via DataTransferService
players = [], // Populated during import via DataTransferService
notes = json['notes'];
/// Converts the Match instance to a JSON object.
/// Converts the Match instance to a JSON object using normalized format (ID references only).
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'players': players?.map((player) => player.toJson()).toList(),
'group': group?.toJson(),
'winner': winner?.toJson(),
'gameId': game?.id,
'groupId': group?.id,
'playerIds': (players ?? []).map((player) => player.id).toList(),
'notes': notes,
};
}

View File

@@ -5,26 +5,33 @@ class Player {
final String id;
final DateTime createdAt;
final String name;
final String? description;
Player({String? id, DateTime? createdAt, required this.name})
: id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
Player({
String? id,
DateTime? createdAt,
required this.name,
this.description,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
String toString() {
return 'Player{id: $id,name: $name}';
return 'Player{id: $id, name: $name, description: $description}';
}
/// Creates a Player instance from a JSON object.
Player.fromJson(Map<String, dynamic> json)
: id = json['id'],
createdAt = DateTime.parse(json['createdAt']),
name = json['name'];
name = json['name'],
description = json['description'];
/// Converts the Player instance to a JSON object.
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt.toIso8601String(),
'name': name,
'description': description,
};
}

40
lib/data/dto/team.dart Normal file
View File

@@ -0,0 +1,40 @@
import 'package:clock/clock.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:uuid/uuid.dart';
class Team {
final String id;
final String name;
final DateTime createdAt;
final List<Player> members;
Team({
String? id,
required this.name,
DateTime? createdAt,
required this.members,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@override
String toString() {
return 'Team{id: $id, name: $name, members: $members}';
}
/// Creates a Team instance from a JSON object (memberIds format).
/// Player objects are reconstructed from memberIds by the DataTransferService.
Team.fromJson(Map<String, dynamic> json)
: id = json['id'],
name = json['name'],
createdAt = DateTime.parse(json['createdAt']),
members = []; // Populated during import via DataTransferService
/// Converts the Team instance to a JSON object using normalized format (memberIds only).
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'createdAt': createdAt.toIso8601String(),
'memberIds': members.map((member) => member.id).toList(),
};
}

View File

@@ -10,7 +10,6 @@
"choose_group": "Gruppe wählen",
"choose_ruleset": "Regelwerk wählen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"create_game": "Spielvorlage erstellen",
"create_group": "Gruppe erstellen",
"create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen",
@@ -23,10 +22,7 @@
"days_ago": "vor {count} Tagen",
"delete": "Löschen",
"delete_all_data": "Alle Daten löschen",
"delete_game": "Spielvorlage löschen",
"delete_group": "Gruppe löschen",
"description": "Beschreibung",
"edit_game": "Spielvorlage bearbeiten",
"edit_group": "Gruppe bearbeiten",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
"error_reading_file": "Fehler beim Lesen der Datei",

View File

@@ -30,9 +30,6 @@
"@could_not_add_player": {
"description": "Error message when adding a player fails"
},
"@create_game": {
"description": "Button text to create a game"
},
"@create_group": {
"description": "Button text to create a group"
},
@@ -74,18 +71,9 @@
"@delete_all_data": {
"description": "Confirmation dialog for deleting all data"
},
"@delete_game": {
"description": "Button text to delete a game"
},
"@delete_group": {
"description": "Button text to delete a group"
},
"description": {
"description": "Description label"
},
"edit_game": {
"description": "Button text to edit a game"
},
"@edit_group": {
"description": "Button text to edit a group"
},
@@ -320,7 +308,6 @@
"choose_group": "Choose Group",
"choose_ruleset": "Choose Ruleset",
"could_not_add_player": "Could not add player",
"create_game": "Create Game",
"create_group": "Create Group",
"create_match": "Create match",
"create_new_group": "Create new group",
@@ -333,10 +320,7 @@
"days_ago": "{count} days ago",
"delete": "Delete",
"delete_all_data": "Delete all data",
"delete_game": "Delete Game",
"delete_group": "Delete Group",
"description": "Description",
"edit_game": "Edit Game",
"edit_group": "Edit Group",
"error_creating_group": "Error while creating group, please try again",
"error_reading_file": "Error reading file",

View File

@@ -98,18 +98,6 @@ abstract class AppLocalizations {
Locale('en'),
];
/// No description provided for @description.
///
/// In en, this message translates to:
/// **'Description'**
String get description;
/// No description provided for @edit_game.
///
/// In en, this message translates to:
/// **'Edit Game'**
String get edit_game;
/// Label for all players list
///
/// In en, this message translates to:
@@ -170,12 +158,6 @@ abstract class AppLocalizations {
/// **'Could not add player'**
String could_not_add_player(Object playerName);
/// Button text to create a game
///
/// In en, this message translates to:
/// **'Create Game'**
String get create_game;
/// Button text to create a group
///
/// In en, this message translates to:
@@ -248,12 +230,6 @@ abstract class AppLocalizations {
/// **'Delete all data'**
String get delete_all_data;
/// Button text to delete a game
///
/// In en, this message translates to:
/// **'Delete Game'**
String get delete_game;
/// Button text to delete a group
///
/// In en, this message translates to:

View File

@@ -8,12 +8,6 @@ import 'app_localizations.dart';
class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get description => 'Beschreibung';
@override
String get edit_game => 'Spielvorlage bearbeiten';
@override
String get all_players => 'Alle Spieler:innen';
@@ -46,9 +40,6 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
}
@override
String get create_game => 'Spielvorlage erstellen';
@override
String get create_group => 'Gruppe erstellen';
@@ -87,9 +78,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get delete_all_data => 'Alle Daten löschen';
@override
String get delete_game => 'Spielvorlage löschen';
@override
String get delete_group => 'Gruppe löschen';

View File

@@ -8,12 +8,6 @@ import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get description => 'Description';
@override
String get edit_game => 'Edit Game';
@override
String get all_players => 'All players';
@@ -46,9 +40,6 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Could not add player';
}
@override
String get create_game => 'Create Game';
@override
String get create_group => 'Create Group';
@@ -87,9 +78,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get delete_all_data => 'Delete all data';
@override
String get delete_game => 'Delete Game';
@override
String get delete_group => 'Delete Group';

View File

@@ -1,10 +1,7 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/game_view/create_game_view.dart';
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
@@ -54,17 +51,6 @@ class _ChooseGameViewState extends State<ChooseGameView> {
Navigator.of(context).pop(selectedGameIndex);
},
),
actions: [IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
await Navigator.push(context, adaptivePageRoute(
builder: (context) => CreateGameView(
callback: () {}, //TODO: implement callback
),
)
);
},
)],
title: Text(loc.choose_game),
),
body: PopScope(
@@ -99,7 +85,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
context,
),
isHighlighted: selectedGameIndex == index,
onTap: () async {
onPressed: () async {
setState(() {
if (selectedGameIndex == index) {
selectedGameIndex = -1;
@@ -108,16 +94,6 @@ class _ChooseGameViewState extends State<ChooseGameView> {
}
});
},
onLongPress: () async {
await Navigator.push(context, adaptivePageRoute(
builder: (context) => CreateGameView(
//TODO: implement callback & giving real game to create game view
gameToEdit: Game(name: 'Cabo', description: '', ruleset: 'Highest Points'),
callback: () {},
),
)
);
},
);
},
),

View File

@@ -4,6 +4,7 @@ import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
@@ -192,11 +193,34 @@ class _CreateMatchViewState extends State<CreateMatchView> {
buttonType: ButtonType.primary,
onPressed: _enableCreateGameButton()
? () async {
// Use a game from the games list
Game? gameToUse;
if (selectedGameIndex == -1) {
// Use the first game as default if none selected
final selectedGame = games[0];
gameToUse = Game(
name: selectedGame.$1,
description: selectedGame.$2,
ruleset: selectedGame.$3.name,
);
} else {
// Use the selected game from the list
final selectedGame = games[selectedGameIndex];
gameToUse = Game(
name: selectedGame.$1,
description: selectedGame.$2,
ruleset: selectedGame.$3.name,
);
}
// Add the game to the database if it doesn't exist
await db.gameDao.addGame(game: gameToUse);
Match match = Match(
name: _matchNameController.text.isEmpty
? (hintText ?? '')
: _matchNameController.text.trim(),
createdAt: DateTime.now(),
game: gameToUse,
group: selectedGroup,
players: selectedPlayers,
);

View File

@@ -1,93 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseRulesetView extends StatefulWidget {
/// A view that allows the user to choose a ruleset from a list of available rulesets
/// - [rulesets]: A list of tuples containing the ruleset and its description
/// - [initialRulesetIndex]: The index of the initially selected ruleset
const ChooseRulesetView({
super.key,
required this.rulesets,
required this.initialRulesetIndex,
});
/// A list of tuples containing the ruleset and its description
final List<(Ruleset, String)> rulesets;
/// The index of the initially selected ruleset
final int initialRulesetIndex;
@override
State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
}
class _ChooseRulesetViewState extends State<ChooseRulesetView> {
/// Currently selected ruleset index
late int selectedRulesetIndex;
@override
void initState() {
selectedRulesetIndex = widget.initialRulesetIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
selectedRulesetIndex == -1
? null
: widget.rulesets[selectedRulesetIndex].$1,
);
},
),
title: Text(loc.choose_ruleset),
),
body: PopScope(
// This fixes that the Android Back Gesture didn't return the
// selectedRulesetIndex and therefore the selected Ruleset wasn't saved
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (didPop) {
return;
}
Navigator.of(context).pop(
selectedRulesetIndex == -1
? null
: widget.rulesets[selectedRulesetIndex].$1,
);
},
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.rulesets.length,
itemBuilder: (BuildContext context, int index) {
return TitleDescriptionListTile(
onTap: () async {
setState(() {
if (selectedRulesetIndex == index) {
selectedRulesetIndex = -1;
} else {
selectedRulesetIndex = index;
}
});
},
title: translateRulesetToString(
widget.rulesets[index].$1,
context,
),
description: widget.rulesets[index].$2,
isHighlighted: selectedRulesetIndex == index,
);
},
),
),
),
);
}
}

View File

@@ -1,140 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/game_view/choose_ruleset_view.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart';
class CreateGameView extends StatefulWidget {
const CreateGameView({super.key, this.gameToEdit, required this.callback});
final Game? gameToEdit;
final VoidCallback callback;
@override
State<CreateGameView> createState() => _CreateGameViewState();
}
class _CreateGameViewState extends State<CreateGameView> {
Ruleset? selectedRuleset;
int selectedRulesetIndex = -1;
late List<(Ruleset, String)> _rulesets;
final _gameNameController = TextEditingController();
final _descriptionController = TextEditingController();
@override
void initState() {
super.initState();
_gameNameController.addListener(() => setState(() {}));
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final loc = AppLocalizations.of(context);
_rulesets = [
(Ruleset.singleWinner, loc.ruleset_single_winner),
(Ruleset.singleLoser, loc.ruleset_single_loser),
(Ruleset.mostPoints, loc.ruleset_most_points),
(Ruleset.leastPoints, loc.ruleset_least_points),
];
if (widget.gameToEdit != null) {
_gameNameController.text = widget.gameToEdit!.name;
_descriptionController.text = widget.gameToEdit!.description ?? '';
// TODO: Handle ruleset initialization from gameToEdit
}
}
@override
void dispose() {
_gameNameController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var loc = AppLocalizations.of(context);
final isEditing = widget.gameToEdit != null;
return ScaffoldMessenger(
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
title: Text(isEditing ? loc.edit_game : loc.create_game),
),
body: SafeArea(
child: Column(
children: [
Container(
margin: CustomTheme.tileMargin,
child: TextInputField(
controller: _gameNameController,
maxLength: Constants.MAX_MATCH_NAME_LENGTH,
hintText: loc.game_name,
),
),
ChooseTile(
title: loc.ruleset,
trailingText: selectedRuleset == null
? loc.none
: translateRulesetToString(selectedRuleset!, context),
onPressed: () async {
final result = await Navigator.of(context).push<Ruleset?>(
adaptivePageRoute(
builder: (context) => ChooseRulesetView(
rulesets: _rulesets,
initialRulesetIndex: selectedRulesetIndex,
),
),
);
if (mounted) {
setState(() {
selectedRuleset = result;
selectedRulesetIndex =
result == null ? -1 : _rulesets.indexWhere((r) => r.$1 == result);
});
}
},
),
Container(
margin: CustomTheme.tileMargin,
child: TextInputField(
controller: _descriptionController,
hintText: loc.description,
minLines: 6,
maxLines: 6,
maxLength: Constants.MAX_GAME_DESCRIPTION_LENGTH,
showCounterText: true,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(12.0),
child: CustomWidthButton(
text: isEditing ? loc.edit_group : loc.create_game,
sizeRelativeToWidth: 1,
buttonType: ButtonType.primary,
onPressed: _gameNameController.text.trim().isNotEmpty && selectedRulesetIndex != -1
? () {
//TODO: Handle saving to db & updating game selection view
Navigator.of(context).pop();
}
: null,
),
),
],
),
),
),
);
}
}

View File

@@ -8,18 +8,12 @@ class TextInputField extends StatelessWidget {
/// - [onChanged]: Optional callback invoked when the text in the field changes.
/// - [hintText]: The hint text displayed in the text input field when it is empty
/// - [maxLength]: Optional parameter for maximum length of the input text.
/// - [maxLines]: The maximum number of lines for the text input field. Defaults to 1.
/// - [minLines]: The minimum number of lines for the text input field. Defaults to 1.
/// - [showCounterText]: Whether to show the counter text in the text input field. Defaults to false.
const TextInputField({
super.key,
required this.controller,
required this.hintText,
this.onChanged,
this.maxLength,
this.maxLines = 1,
this.minLines = 1,
this.showCounterText = false
});
/// The controller for the text input field.
@@ -34,15 +28,6 @@ class TextInputField extends StatelessWidget {
/// Optional parameter for maximum length of the input text.
final int? maxLength;
/// The maximum number of lines for the text input field.
final int? maxLines;
/// The minimum number of lines for the text input field.
final int? minLines;
/// Whether to show the counter text in the text input field.
final bool showCounterText;
@override
Widget build(BuildContext context) {
return TextField(
@@ -50,14 +35,13 @@ class TextInputField extends StatelessWidget {
onChanged: onChanged,
maxLength: maxLength,
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
maxLines: maxLines,
minLines: minLines,
decoration: InputDecoration(
filled: true,
fillColor: CustomTheme.boxColor,
hintText: hintText,
hintStyle: const TextStyle(fontSize: 18),
counterText: showCounterText ? null : '',
// Hides the character counter
counterText: '',
enabledBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorder),

View File

@@ -6,7 +6,6 @@ class TitleDescriptionListTile extends StatelessWidget {
/// - [title]: The title text displayed on the tile.
/// - [description]: The description text displayed below the title.
/// - [onPressed]: The callback invoked when the tile is tapped.
/// - [onLongPress]: The callback invoked when the tile is tapped.
/// - [isHighlighted]: A boolean to determine if the tile should be highlighted.
/// - [badgeText]: Optional text to display in a badge on the right side of the title.
/// - [badgeColor]: Optional color for the badge background.
@@ -14,8 +13,7 @@ class TitleDescriptionListTile extends StatelessWidget {
super.key,
required this.title,
required this.description,
this.onTap,
this.onLongPress,
this.onPressed,
this.isHighlighted = false,
this.badgeText,
this.badgeColor,
@@ -28,10 +26,7 @@ class TitleDescriptionListTile extends StatelessWidget {
final String description;
/// The callback invoked when the tile is tapped.
final VoidCallback? onTap;
/// The callback invoked when the tile is long-pressed.
final VoidCallback? onLongPress;
final VoidCallback? onPressed;
/// A boolean to determine if the tile should be highlighted.
final bool isHighlighted;
@@ -45,8 +40,7 @@ class TitleDescriptionListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
onLongPress: onLongPress,
onTap: onPressed,
child: AnimatedContainer(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),

View File

@@ -6,9 +6,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/data/dto/team.dart';
import 'package:json_schema/json_schema.dart';
import 'package:provider/provider.dart';
@@ -17,39 +19,54 @@ class DataTransferService {
static Future<void> deleteAllData(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false);
await db.matchDao.deleteAllMatches();
await db.teamDao.deleteAllTeams();
await db.groupDao.deleteAllGroups();
await db.gameDao.deleteAllGames();
await db.playerDao.deleteAllPlayers();
}
/// Retrieves all application data and converts it to a JSON string.
/// Returns the JSON string representation of the data.
/// Returns the JSON string representation of the data in normalized format.
static Future<String> getAppDataAsJson(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false);
final matches = await db.matchDao.getAllMatches();
final groups = await db.groupDao.getAllGroups();
final players = await db.playerDao.getAllPlayers();
final games = await db.gameDao.getAllGames();
final teams = await db.teamDao.getAllTeams();
// Construct a JSON representation of the data
// Construct a JSON representation of the data in normalized format
final Map<String, dynamic> jsonMap = {
'players': players.map((p) => p.toJson()).toList(),
'games': games.map((g) => g.toJson()).toList(),
'groups': groups
.map((g) => {
'id': g.id,
'name': g.name,
'createdAt': g.createdAt.toIso8601String(),
'memberIds': (g.members).map((m) => m.id).toList(),
}).toList(),
'id': g.id,
'name': g.name,
'description': g.description,
'createdAt': g.createdAt.toIso8601String(),
'memberIds': (g.members).map((m) => m.id).toList(),
})
.toList(),
'teams': teams
.map((t) => {
'id': t.id,
'name': t.name,
'createdAt': t.createdAt.toIso8601String(),
'memberIds': (t.members).map((m) => m.id).toList(),
})
.toList(),
'matches': matches
.map((m) => {
'id': m.id,
'name': m.name,
'createdAt': m.createdAt.toIso8601String(),
'groupId': m.group?.id,
'playerIds': (m.players ?? []).map((p) => p.id).toList(),
'winnerId': m.winner?.id,
}).toList(),
'id': m.id,
'name': m.name,
'createdAt': m.createdAt.toIso8601String(),
'gameId': m.game?.id,
'groupId': m.group?.id,
'playerIds': (m.players ?? []).map((p) => p.id).toList(),
'notes': m.notes,
})
.toList(),
};
return json.encode(jsonMap);
@@ -107,10 +124,12 @@ class DataTransferService {
final Map<String, dynamic> decoded = json.decode(jsonString) as Map<String, dynamic>;
final List<dynamic> playersJson = (decoded['players'] as List<dynamic>?) ?? [];
final List<dynamic> gamesJson = (decoded['games'] as List<dynamic>?) ?? [];
final List<dynamic> groupsJson = (decoded['groups'] as List<dynamic>?) ?? [];
final List<dynamic> teamsJson = (decoded['teams'] as List<dynamic>?) ?? [];
final List<dynamic> matchesJson = (decoded['matches'] as List<dynamic>?) ?? [];
// Players
// Import Players
final List<Player> importedPlayers = playersJson
.map((p) => Player.fromJson(p as Map<String, dynamic>))
.toList();
@@ -119,7 +138,16 @@ class DataTransferService {
for (final p in importedPlayers) p.id: p,
};
// Groups
// Import Games
final List<Game> importedGames = gamesJson
.map((g) => Game.fromJson(g as Map<String, dynamic>))
.toList();
final Map<String, Game> gameById = {
for (final g in importedGames) g.id: g,
};
// Import Groups
final List<Group> importedGroups = groupsJson.map((g) {
final map = g as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>();
@@ -132,6 +160,7 @@ class DataTransferService {
return Group(
id: map['id'] as String,
name: map['name'] as String,
description: map['description'] as String?,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
@@ -141,33 +170,55 @@ class DataTransferService {
for (final g in importedGroups) g.id: g,
};
// Matches
// Import Teams
final List<Team> importedTeams = teamsJson.map((t) {
final map = t as Map<String, dynamic>;
final memberIds = (map['memberIds'] as List<dynamic>? ?? []).cast<String>();
final members = memberIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
return Team(
id: map['id'] as String,
name: map['name'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
// Import Matches
final List<Match> importedMatches = matchesJson.map((m) {
final map = m as Map<String, dynamic>;
final String? gameId = map['gameId'] as String?;
final String? groupId = map['groupId'] as String?;
final List<String> playerIds = (map['playerIds'] as List<dynamic>? ?? []).cast<String>();
final String? winnerId = map['winnerId'] as String?;
final game = (gameId == null) ? null : gameById[gameId];
final group = (groupId == null) ? null : groupById[groupId];
final players = playerIds
.map((id) => playerById[id])
.whereType<Player>()
.toList();
final winner = (winnerId == null) ? null : playerById[winnerId];
return Match(
id: map['id'] as String,
name: map['name'] as String,
game: game,
group: group,
players: players,
players: players.isNotEmpty ? players : null,
createdAt: DateTime.parse(map['createdAt'] as String),
winner: winner,
notes: map['notes'] as String?,
);
}).toList();
// Import all data into the database
await db.playerDao.addPlayersAsList(players: importedPlayers);
await db.gameDao.addGamesAsList(games: importedGames);
await db.groupDao.addGroupsAsList(groups: importedGroups);
await db.teamDao.addTeamsAsList(teams: importedTeams);
await db.matchDao.addMatchAsList(matches: importedMatches);
return ImportResult.success;

View File

@@ -1,7 +1,7 @@
name: game_tracker
description: "Game Tracking App for Card Games"
publish_to: 'none'
version: 0.0.10+248
version: 0.0.10+237
environment:
sdk: ^3.8.1

View File

@@ -1,362 +1,548 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/data/dto/game.dart';
void main() {
late AppDatabase database;
late Player testPlayer1;
late Player testPlayer2;
late Player testPlayer3;
late Player testPlayer4;
late Player testPlayer5;
late Group testGroup1;
late Group testGroup2;
late Match testMatch1;
late Match testMatch2;
late Match testMatchOnlyPlayers;
late Match testMatchOnlyGroup;
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
late Game testGame1;
late Game testGame2;
late Game testGame3;
final fixedDate = DateTime(2025, 11, 19, 00, 11, 23);
final fakeClock = Clock(() => fixedDate);
setUp(() async {
setUp(() {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
// Recommended for widget tests to avoid test errors.
closeStreamsSynchronously: true,
),
);
withClock(fakeClock, () {
testPlayer1 = Player(name: 'Alice');
testPlayer2 = Player(name: 'Bob');
testPlayer3 = Player(name: 'Charlie');
testPlayer4 = Player(name: 'Diana');
testPlayer5 = Player(name: 'Eve');
testGroup1 = Group(
name: 'Test Group 2',
members: [testPlayer1, testPlayer2, testPlayer3],
testGame1 = Game(
name: 'Chess',
ruleset: 'winner.single',
description: 'A classic strategy game',
color: 0xFF0000FF,
icon: 'chess_icon',
);
testGroup2 = Group(
name: 'Test Group 2',
members: [testPlayer4, testPlayer5],
testGame2 = Game(
id: 'game2',
name: 'Poker',
ruleset: 'Texas Hold\'em rules',
description: 'winner.multiple',
color: 0xFFFF0000,
icon: 'poker_icon',
);
testMatch1 = Match(
name: 'First Test Match',
group: testGroup1,
players: [testPlayer4, testPlayer5],
winner: testPlayer4,
);
testMatch2 = Match(
name: 'Second Test Match',
group: testGroup2,
players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer2,
);
testMatchOnlyPlayers = Match(
name: 'Test Match with Players',
players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer3,
);
testMatchOnlyGroup = Match(
name: 'Test Match with Group',
group: testGroup2,
testGame3 = Game(
id: 'game3',
name: 'Monopoly',
description: 'A board game about real estate',
);
});
await database.playerDao.addPlayersAsList(
players: [
testPlayer1,
testPlayer2,
testPlayer3,
testPlayer4,
testPlayer5,
],
);
await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
});
tearDown(() async {
await database.close();
});
group('Match Tests', () {
test('Adding and fetching single match works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
group('Game Tests', () {
final result = await database.matchDao.getMatchById(
matchId: testMatch1.id,
);
expect(result.id, testMatch1.id);
expect(result.name, testMatch1.name);
expect(result.createdAt, testMatch1.createdAt);
if (result.winner != null && testMatch1.winner != null) {
expect(result.winner!.id, testMatch1.winner!.id);
expect(result.winner!.name, testMatch1.winner!.name);
expect(result.winner!.createdAt, testMatch1.winner!.createdAt);
} else {
expect(result.winner, testMatch1.winner);
}
if (result.group != null) {
expect(result.group!.members.length, testGroup1.members.length);
for (int i = 0; i < testGroup1.members.length; i++) {
expect(result.group!.members[i].id, testGroup1.members[i].id);
expect(result.group!.members[i].name, testGroup1.members[i].name);
}
} else {
fail('Group is null');
}
if (result.players != null) {
expect(result.players!.length, testMatch1.players!.length);
for (int i = 0; i < testMatch1.players!.length; i++) {
expect(result.players![i].id, testMatch1.players![i].id);
expect(result.players![i].name, testMatch1.players![i].name);
expect(
result.players![i].createdAt,
testMatch1.players![i].createdAt,
);
}
} else {
fail('Players is null');
}
// Verifies that getAllGames returns an empty list when the database has no games.
test('getAllGames returns empty list when no games exist', () async {
final allGames = await database.gameDao.getAllGames();
expect(allGames, isEmpty);
});
test('Adding and fetching multiple matches works correctly', () async {
await database.matchDao.addMatchAsList(
matches: [
testMatch1,
testMatch2,
testMatchOnlyGroup,
testMatchOnlyPlayers,
],
);
// Verifies that a single game can be added and retrieved with all fields intact.
test('Adding and fetching a single game works correctly', () async {
await database.gameDao.addGame(game: testGame1);
final allMatches = await database.matchDao.getAllMatches();
expect(allMatches.length, 4);
final testMatches = {
testMatch1.id: testMatch1,
testMatch2.id: testMatch2,
testMatchOnlyGroup.id: testMatchOnlyGroup,
testMatchOnlyPlayers.id: testMatchOnlyPlayers,
};
for (final match in allMatches) {
final testMatch = testMatches[match.id]!;
// Match-Checks
expect(match.id, testMatch.id);
expect(match.name, testMatch.name);
expect(match.createdAt, testMatch.createdAt);
if (match.winner != null && testMatch.winner != null) {
expect(match.winner!.id, testMatch.winner!.id);
expect(match.winner!.name, testMatch.winner!.name);
expect(match.winner!.createdAt, testMatch.winner!.createdAt);
} else {
expect(match.winner, testMatch.winner);
}
// Group-Checks
if (testMatch.group != null) {
expect(match.group!.id, testMatch.group!.id);
expect(match.group!.name, testMatch.group!.name);
expect(match.group!.createdAt, testMatch.group!.createdAt);
// Group Members-Checks
expect(match.group!.members.length, testMatch.group!.members.length);
for (int i = 0; i < testMatch.group!.members.length; i++) {
expect(match.group!.members[i].id, testMatch.group!.members[i].id);
expect(
match.group!.members[i].name,
testMatch.group!.members[i].name,
);
expect(
match.group!.members[i].createdAt,
testMatch.group!.members[i].createdAt,
);
}
} else {
expect(match.group, null);
}
// Players-Checks
if (testMatch.players != null) {
expect(match.players!.length, testMatch.players!.length);
for (int i = 0; i < testMatch.players!.length; i++) {
expect(match.players![i].id, testMatch.players![i].id);
expect(match.players![i].name, testMatch.players![i].name);
expect(
match.players![i].createdAt,
testMatch.players![i].createdAt,
);
}
} else {
expect(match.players, null);
}
}
final allGames = await database.gameDao.getAllGames();
expect(allGames.length, 1);
expect(allGames.first.id, testGame1.id);
expect(allGames.first.name, testGame1.name);
expect(allGames.first.ruleset, testGame1.ruleset);
expect(allGames.first.description, testGame1.description);
expect(allGames.first.color, testGame1.color);
expect(allGames.first.icon, testGame1.icon);
expect(allGames.first.createdAt, testGame1.createdAt);
});
test('Adding the same match twice does not create duplicates', () async {
await database.matchDao.addMatch(match: testMatch1);
await database.matchDao.addMatch(match: testMatch1);
// Verifies that multiple games can be added and retrieved correctly.
test('Adding and fetching multiple games works correctly', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.addGame(game: testGame2);
await database.gameDao.addGame(game: testGame3);
final matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 1);
final allGames = await database.gameDao.getAllGames();
expect(allGames.length, 3);
final names = allGames.map((g) => g.name).toList();
expect(names, containsAll(['Chess', 'Poker', 'Monopoly']));
});
test('Match existence check works correctly', () async {
var matchExists = await database.matchDao.matchExists(
matchId: testMatch1.id,
);
expect(matchExists, false);
// Verifies that getGameById returns the correct game with all properties.
test('getGameById returns correct game', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.addGame(game: testGame2);
await database.matchDao.addMatch(match: testMatch1);
matchExists = await database.matchDao.matchExists(matchId: testMatch1.id);
expect(matchExists, true);
final game = await database.gameDao.getGameById(gameId: testGame2.id);
expect(game.id, testGame2.id);
expect(game.name, testGame2.name);
expect(game.ruleset, testGame2.ruleset);
expect(game.description, testGame2.description);
expect(game.color, testGame2.color);
expect(game.icon, testGame2.icon);
});
test('Deleting a match works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final matchDeleted = await database.matchDao.deleteMatch(
matchId: testMatch1.id,
// Verifies that getGameById throws a StateError when the game doesn't exist.
test('getGameById throws exception for non-existent game', () async {
expect(
() => database.gameDao.getGameById(gameId: 'non-existent-id'),
throwsA(isA<StateError>()),
);
expect(matchDeleted, true);
final matchExists = await database.matchDao.matchExists(
matchId: testMatch1.id,
);
expect(matchExists, false);
});
test('Getting the match count works correctly', () async {
var matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 0);
// Verifies that addGame returns true when a game is successfully added.
test('addGame returns true when game is added successfully', () async {
final result = await database.gameDao.addGame(game: testGame1);
expect(result, true);
await database.matchDao.addMatch(match: testMatch1);
matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 1);
await database.matchDao.addMatch(match: testMatch2);
matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 2);
await database.matchDao.deleteMatch(matchId: testMatch1.id);
matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 1);
await database.matchDao.deleteMatch(matchId: testMatch2.id);
matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 0);
final allGames = await database.gameDao.getAllGames();
expect(allGames.length, 1);
});
test('Checking if match has winner works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
await database.matchDao.addMatch(match: testMatchOnlyGroup);
// Verifies that addGame returns false when trying to add a duplicate game.
test('addGame returns false when game already exists', () async {
final firstAdd = await database.gameDao.addGame(game: testGame1);
expect(firstAdd, true);
var hasWinner = await database.matchDao.hasWinner(matchId: testMatch1.id);
expect(hasWinner, true);
final secondAdd = await database.gameDao.addGame(game: testGame1);
expect(secondAdd, false);
hasWinner = await database.matchDao.hasWinner(
matchId: testMatchOnlyGroup.id,
);
expect(hasWinner, false);
final allGames = await database.gameDao.getAllGames();
expect(allGames.length, 1);
});
test('Fetching the winner of a match works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
// Verifies that a game with null optional fields can be added and retrieved.
test('addGame handles game with null optional fields', () async {
final gameWithNulls = Game(name: 'Simple Game');
final result = await database.gameDao.addGame(game: gameWithNulls);
expect(result, true);
final winner = await database.matchDao.getWinner(matchId: testMatch1.id);
if (winner == null) {
fail('Winner is null');
} else {
expect(winner.id, testMatch1.winner!.id);
expect(winner.name, testMatch1.winner!.name);
expect(winner.createdAt, testMatch1.winner!.createdAt);
}
final fetchedGame = await database.gameDao.getGameById(
gameId: gameWithNulls.id,
);
expect(fetchedGame.name, 'Simple Game');
expect(fetchedGame.description, isNull);
expect(fetchedGame.color, isNull);
expect(fetchedGame.icon, isNull);
});
test('Updating the winner of a match works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final winner = await database.matchDao.getWinner(matchId: testMatch1.id);
if (winner == null) {
fail('Winner is null');
} else {
expect(winner.id, testMatch1.winner!.id);
expect(winner.name, testMatch1.winner!.name);
expect(winner.createdAt, testMatch1.winner!.createdAt);
expect(winner.id, testPlayer4.id);
expect(winner.id != testPlayer5.id, true);
}
await database.matchDao.setWinner(
matchId: testMatch1.id,
winnerId: testPlayer5.id,
// Verifies that multiple games can be added at once using addGamesAsList.
test('addGamesAsList adds multiple games correctly', () async {
final result = await database.gameDao.addGamesAsList(
games: [testGame1, testGame2, testGame3],
);
expect(result, true);
final newWinner = await database.matchDao.getWinner(
matchId: testMatch1.id,
);
if (newWinner == null) {
fail('New winner is null');
} else {
expect(newWinner.id, testPlayer5.id);
expect(newWinner.name, testPlayer5.name);
expect(newWinner.createdAt, testPlayer5.createdAt);
}
final allGames = await database.gameDao.getAllGames();
expect(allGames.length, 3);
});
test('Removing a winner works correctly', () async {
await database.matchDao.addMatch(match: testMatch2);
// Verifies that addGamesAsList returns false when given an empty list.
test('addGamesAsList returns false for empty list', () async {
final result = await database.gameDao.addGamesAsList(games: []);
expect(result, false);
var hasWinner = await database.matchDao.hasWinner(matchId: testMatch2.id);
expect(hasWinner, true);
await database.matchDao.removeWinner(matchId: testMatch2.id);
hasWinner = await database.matchDao.hasWinner(matchId: testMatch2.id);
expect(hasWinner, false);
final removedWinner = await database.matchDao.getWinner(
matchId: testMatch2.id,
);
expect(removedWinner, null);
final allGames = await database.gameDao.getAllGames();
expect(allGames.length, 0);
});
test('Renaming a match works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
// Verifies that addGamesAsList ignores duplicate games when adding.
test('addGamesAsList ignores duplicate games', () async {
await database.gameDao.addGame(game: testGame1);
var fetchedMatch = await database.matchDao.getMatchById(
matchId: testMatch1.id,
final result = await database.gameDao.addGamesAsList(
games: [testGame1, testGame2],
);
expect(fetchedMatch.name, testMatch1.name);
expect(result, true);
const newName = 'Updated Match Name';
await database.matchDao.updateMatchName(
matchId: testMatch1.id,
newName: newName,
final allGames = await database.gameDao.getAllGames();
expect(allGames.length, 2);
});
// Verifies that deleteGame returns true and removes the game from database.
test('deleteGame returns true when game is deleted', () async {
await database.gameDao.addGame(game: testGame1);
final result = await database.gameDao.deleteGame(gameId: testGame1.id);
expect(result, true);
final allGames = await database.gameDao.getAllGames();
expect(allGames, isEmpty);
});
// Verifies that deleteGame returns false for a non-existent game ID.
test('deleteGame returns false for non-existent game', () async {
final result = await database.gameDao.deleteGame(
gameId: 'non-existent-id',
);
expect(result, false);
});
// Verifies that deleteGame only removes the specified game, leaving others intact.
test('deleteGame only deletes the specified game', () async {
await database.gameDao.addGamesAsList(
games: [testGame1, testGame2, testGame3],
);
fetchedMatch = await database.matchDao.getMatchById(
matchId: testMatch1.id,
await database.gameDao.deleteGame(gameId: testGame2.id);
final allGames = await database.gameDao.getAllGames();
expect(allGames.length, 2);
expect(allGames.any((g) => g.id == testGame2.id), false);
expect(allGames.any((g) => g.id == testGame1.id), true);
expect(allGames.any((g) => g.id == testGame3.id), true);
});
// Verifies that gameExists returns true when the game exists in database.
test('gameExists returns true for existing game', () async {
await database.gameDao.addGame(game: testGame1);
final exists = await database.gameDao.gameExists(gameId: testGame1.id);
expect(exists, true);
});
// Verifies that gameExists returns false for a non-existent game ID.
test('gameExists returns false for non-existent game', () async {
final exists = await database.gameDao.gameExists(
gameId: 'non-existent-id',
);
expect(fetchedMatch.name, newName);
expect(exists, false);
});
// Verifies that gameExists returns false after a game has been deleted.
test('gameExists returns false after game is deleted', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.deleteGame(gameId: testGame1.id);
final exists = await database.gameDao.gameExists(gameId: testGame1.id);
expect(exists, false);
});
// Verifies that updateGameName correctly updates only the name field.
test('updateGameName updates the name correctly', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.updateGameName(
gameId: testGame1.id,
newName: 'Updated Chess',
);
final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id,
);
expect(updatedGame.name, 'Updated Chess');
expect(updatedGame.ruleset, testGame1.ruleset);
});
// Verifies that updateGameName does nothing when game doesn't exist.
test('updateGameName does nothing for non-existent game', () async {
await database.gameDao.updateGameName(
gameId: 'non-existent-id',
newName: 'New Name',
);
final allGames = await database.gameDao.getAllGames();
expect(allGames, isEmpty);
});
// Verifies that updateGameRuleset correctly updates only the ruleset field.
test('updateGameRuleset updates the ruleset correctly', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.updateGameRuleset(
gameId: testGame1.id,
newRuleset: 'New ruleset for chess',
);
final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id,
);
expect(updatedGame.ruleset, 'New ruleset for chess');
expect(updatedGame.name, testGame1.name);
});
// Verifies that updateGameRuleset does nothing when game doesn't exist.
test('updateGameRuleset does nothing for non-existent game', () async {
await database.gameDao.updateGameRuleset(
gameId: 'non-existent-id',
newRuleset: 'New Ruleset',
);
final allGames = await database.gameDao.getAllGames();
expect(allGames, isEmpty);
});
// Verifies that updateGameDescription correctly updates the description.
test('updateGameDescription updates the description correctly', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.updateGameDescription(
gameId: testGame1.id,
newDescription: 'An updated description',
);
final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id,
);
expect(updatedGame.description, 'An updated description');
});
// Verifies that updateGameDescription can set the description to null.
test('updateGameDescription can set description to null', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.updateGameDescription(
gameId: testGame1.id,
newDescription: null,
);
final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id,
);
expect(updatedGame.description, isNull);
});
// Verifies that updateGameDescription does nothing when game doesn't exist.
test('updateGameDescription does nothing for non-existent game', () async {
await database.gameDao.updateGameDescription(
gameId: 'non-existent-id',
newDescription: 'New Description',
);
final allGames = await database.gameDao.getAllGames();
expect(allGames, isEmpty);
});
// Verifies that updateGameColor correctly updates the color value.
test('updateGameColor updates the color correctly', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.updateGameColor(
gameId: testGame1.id,
newColor: 0xFF00FF00,
);
final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id,
);
expect(updatedGame.color, 0xFF00FF00);
});
// Verifies that updateGameColor can set the color to null.
test('updateGameColor can set color to null', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.updateGameColor(
gameId: testGame1.id,
newColor: null,
);
final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id,
);
expect(updatedGame.color, isNull);
});
// Verifies that updateGameColor does nothing when game doesn't exist.
test('updateGameColor does nothing for non-existent game', () async {
await database.gameDao.updateGameColor(
gameId: 'non-existent-id',
newColor: 0xFF00FF00,
);
final allGames = await database.gameDao.getAllGames();
expect(allGames, isEmpty);
});
// Verifies that updateGameIcon correctly updates the icon value.
test('updateGameIcon updates the icon correctly', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.updateGameIcon(
gameId: testGame1.id,
newIcon: 'new_chess_icon',
);
final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id,
);
expect(updatedGame.icon, 'new_chess_icon');
});
// Verifies that updateGameIcon can set the icon to null.
test('updateGameIcon can set icon to null', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.updateGameIcon(
gameId: testGame1.id,
newIcon: null,
);
final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id,
);
expect(updatedGame.icon, isNull);
});
// Verifies that updateGameIcon does nothing when game doesn't exist.
test('updateGameIcon does nothing for non-existent game', () async {
await database.gameDao.updateGameIcon(
gameId: 'non-existent-id',
newIcon: 'some_icon',
);
final allGames = await database.gameDao.getAllGames();
expect(allGames, isEmpty);
});
// Verifies that getGameCount returns 0 when no games exist.
test('getGameCount returns 0 when no games exist', () async {
final count = await database.gameDao.getGameCount();
expect(count, 0);
});
// Verifies that getGameCount returns the correct count after adding games.
test('getGameCount returns correct count after adding games', () async {
await database.gameDao.addGamesAsList(
games: [testGame1, testGame2, testGame3],
);
final count = await database.gameDao.getGameCount();
expect(count, 3);
});
// Verifies that getGameCount updates correctly after deleting a game.
test('getGameCount updates correctly after deletion', () async {
await database.gameDao.addGamesAsList(
games: [testGame1, testGame2],
);
final countBefore = await database.gameDao.getGameCount();
expect(countBefore, 2);
await database.gameDao.deleteGame(gameId: testGame1.id);
final countAfter = await database.gameDao.getGameCount();
expect(countAfter, 1);
});
// Verifies that deleteAllGames removes all games from the database.
test('deleteAllGames removes all games', () async {
await database.gameDao.addGamesAsList(
games: [testGame1, testGame2, testGame3],
);
final countBefore = await database.gameDao.getGameCount();
expect(countBefore, 3);
final result = await database.gameDao.deleteAllGames();
expect(result, true);
final countAfter = await database.gameDao.getGameCount();
expect(countAfter, 0);
});
// Verifies that deleteAllGames returns false when no games exist.
test('deleteAllGames returns false when no games exist', () async {
final result = await database.gameDao.deleteAllGames();
expect(result, false);
});
// Verifies that games with special characters (quotes, emojis) are stored correctly.
test('Game with special characters in name is stored correctly', () async {
final specialGame = Game(
name: 'Game\'s & "Special" <Name>',
description: 'Description with émojis 🎮🎲',
);
await database.gameDao.addGame(game: specialGame);
final fetchedGame = await database.gameDao.getGameById(
gameId: specialGame.id,
);
expect(fetchedGame.name, 'Game\'s & "Special" <Name>');
expect(fetchedGame.description, 'Description with émojis 🎮🎲');
});
// Verifies that games with empty string fields are stored and retrieved correctly.
test('Game with empty string fields is stored correctly', () async {
final emptyGame = Game(
name: '',
ruleset: '',
description: '',
icon: '',
);
await database.gameDao.addGame(game: emptyGame);
final fetchedGame = await database.gameDao.getGameById(
gameId: emptyGame.id,
);
expect(fetchedGame.name, '');
expect(fetchedGame.ruleset, '');
expect(fetchedGame.description, '');
expect(fetchedGame.icon, '');
});
// Verifies that games with very long strings (10000 chars) are handled correctly.
test('Game with very long strings is stored correctly', () async {
final longString = 'A' * 10000;
final longGame = Game(
name: longString,
description: longString,
ruleset: longString,
);
await database.gameDao.addGame(game: longGame);
final fetchedGame = await database.gameDao.getGameById(
gameId: longGame.id,
);
expect(fetchedGame.name.length, 10000);
expect(fetchedGame.description?.length, 10000);
expect(fetchedGame.ruleset?.length, 10000);
});
// Verifies that multiple sequential updates to the same game work correctly.
test('Multiple updates to the same game work correctly', () async {
await database.gameDao.addGame(game: testGame1);
await database.gameDao.updateGameName(
gameId: testGame1.id,
newName: 'Updated Name',
);
await database.gameDao.updateGameColor(
gameId: testGame1.id,
newColor: 0xFF123456,
);
await database.gameDao.updateGameDescription(
gameId: testGame1.id,
newDescription: 'Updated Description',
);
final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id,
);
expect(updatedGame.name, 'Updated Name');
expect(updatedGame.color, 0xFF123456);
expect(updatedGame.description, 'Updated Description');
expect(updatedGame.ruleset, testGame1.ruleset);
expect(updatedGame.icon, testGame1.icon);
});
});
}

View File

@@ -1,221 +0,0 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' hide isNotNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
void main() {
late AppDatabase database;
late Player testPlayer1;
late Player testPlayer2;
late Player testPlayer3;
late Player testPlayer4;
late Player testPlayer5;
late Group testGroup1;
late Group testGroup2;
late Match testMatchWithGroup;
late Match testMatchWithPlayers;
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
final fakeClock = Clock(() => fixedDate);
setUp(() async {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
// Recommended for widget tests to avoid test errors.
closeStreamsSynchronously: true,
),
);
withClock(fakeClock, () {
testPlayer1 = Player(name: 'Alice');
testPlayer2 = Player(name: 'Bob');
testPlayer3 = Player(name: 'Charlie');
testPlayer4 = Player(name: 'Diana');
testPlayer5 = Player(name: 'Eve');
testGroup1 = Group(
name: 'Test Group',
members: [testPlayer1, testPlayer2, testPlayer3],
);
testGroup2 = Group(
name: 'Test Group',
members: [testPlayer3, testPlayer2],
);
testMatchWithPlayers = Match(
name: 'Test Match with Players',
players: [testPlayer4, testPlayer5],
);
testMatchWithGroup = Match(
name: 'Test Match with Group',
group: testGroup1,
);
});
await database.playerDao.addPlayersAsList(
players: [
testPlayer1,
testPlayer2,
testPlayer3,
testPlayer4,
testPlayer5,
],
);
await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
});
tearDown(() async {
await database.close();
});
group('Group-Match Tests', () {
test('matchHasGroup() has group works correctly', () async {
await database.matchDao.addMatch(match: testMatchWithPlayers);
await database.groupDao.addGroup(group: testGroup1);
var matchHasGroup = await database.groupMatchDao.matchHasGroup(
matchId: testMatchWithPlayers.id,
);
expect(matchHasGroup, false);
await database.groupMatchDao.addGroupToMatch(
matchId: testMatchWithPlayers.id,
groupId: testGroup1.id,
);
matchHasGroup = await database.groupMatchDao.matchHasGroup(
matchId: testMatchWithPlayers.id,
);
expect(matchHasGroup, true);
});
test('Adding a group to a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchWithPlayers);
await database.groupDao.addGroup(group: testGroup1);
await database.groupMatchDao.addGroupToMatch(
matchId: testMatchWithPlayers.id,
groupId: testGroup1.id,
);
var groupAdded = await database.groupMatchDao.isGroupInMatch(
matchId: testMatchWithPlayers.id,
groupId: testGroup1.id,
);
expect(groupAdded, true);
groupAdded = await database.groupMatchDao.isGroupInMatch(
matchId: testMatchWithPlayers.id,
groupId: '',
);
expect(groupAdded, false);
});
test('Removing group from match works correctly', () async {
await database.matchDao.addMatch(match: testMatchWithGroup);
final groupToRemove = testMatchWithGroup.group!;
final removed = await database.groupMatchDao.removeGroupFromMatch(
groupId: groupToRemove.id,
matchId: testMatchWithGroup.id,
);
expect(removed, true);
final result = await database.matchDao.getMatchById(
matchId: testMatchWithGroup.id,
);
expect(result.group, null);
});
test('Retrieving group of a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchWithGroup);
final group = await database.groupMatchDao.getGroupOfMatch(
matchId: testMatchWithGroup.id,
);
if (group == null) {
fail('Group should not be null');
}
expect(group.id, testGroup1.id);
expect(group.name, testGroup1.name);
expect(group.createdAt, testGroup1.createdAt);
expect(group.members.length, testGroup1.members.length);
for (int i = 0; i < group.members.length; i++) {
expect(group.members[i].id, testGroup1.members[i].id);
expect(group.members[i].name, testGroup1.members[i].name);
expect(group.members[i].createdAt, testGroup1.members[i].createdAt);
}
});
test('Updating the group of a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchWithGroup);
var group = await database.groupMatchDao.getGroupOfMatch(
matchId: testMatchWithGroup.id,
);
if (group == null) {
fail('Initial group should not be null');
} else {
expect(group.id, testGroup1.id);
expect(group.name, testGroup1.name);
expect(group.createdAt, testGroup1.createdAt);
expect(group.members.length, testGroup1.members.length);
}
await database.groupDao.addGroup(group: testGroup2);
await database.groupMatchDao.updateGroupOfMatch(
matchId: testMatchWithGroup.id,
newGroupId: testGroup2.id,
);
group = await database.groupMatchDao.getGroupOfMatch(
matchId: testMatchWithGroup.id,
);
if (group == null) {
fail('Updated group should not be null');
} else {
expect(group.id, testGroup2.id);
expect(group.name, testGroup2.name);
expect(group.createdAt, testGroup2.createdAt);
expect(group.members.length, testGroup2.members.length);
for (int i = 0; i < group.members.length; i++) {
expect(group.members[i].id, testGroup2.members[i].id);
expect(group.members[i].name, testGroup2.members[i].name);
expect(group.members[i].createdAt, testGroup2.members[i].createdAt);
}
}
});
test('Adding the same group to seperate matches works correctly', () async {
final match1 = Match(name: 'Match 1', group: testGroup1);
final match2 = Match(name: 'Match 2', group: testGroup1);
await Future.wait([
database.matchDao.addMatch(match: match1),
database.matchDao.addMatch(match: match2),
]);
final group1 = await database.groupMatchDao.getGroupOfMatch(
matchId: match1.id,
);
final group2 = await database.groupMatchDao.getGroupOfMatch(
matchId: match2.id,
);
expect(group1, isNotNull);
expect(group2, isNotNull);
final groups = [group1!, group2!];
for (final group in groups) {
expect(group.members.length, testGroup1.members.length);
expect(group.id, testGroup1.id);
expect(group.name, testGroup1.name);
expect(group.createdAt, testGroup1.createdAt);
}
});
});
}

View File

@@ -1,5 +1,5 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
@@ -58,6 +58,8 @@ void main() {
await database.close();
});
group('Group Tests', () {
// Verifies that a single group can be added and retrieved with all fields and members intact.
test('Adding and fetching a single group works correctly', () async {
await database.groupDao.addGroup(group: testGroup1);
@@ -80,6 +82,7 @@ void main() {
}
});
// Verifies that multiple groups can be added and retrieved with correct members.
test('Adding and fetching multiple groups works correctly', () async {
await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup2, testGroup3, testGroup4],
@@ -106,6 +109,7 @@ void main() {
}
});
// Verifies that adding the same group twice does not create duplicates.
test('Adding the same group twice does not create duplicates', () async {
await database.groupDao.addGroup(group: testGroup1);
await database.groupDao.addGroup(group: testGroup1);
@@ -114,6 +118,7 @@ void main() {
expect(allGroups.length, 1);
});
// Verifies that groupExists returns correct boolean based on group presence.
test('Group existence check works correctly', () async {
var groupExists = await database.groupDao.groupExists(
groupId: testGroup1.id,
@@ -126,6 +131,7 @@ void main() {
expect(groupExists, true);
});
// Verifies that deleteGroup removes the group and returns true.
test('Deleting a group works correctly', () async {
await database.groupDao.addGroup(group: testGroup1);
@@ -140,12 +146,13 @@ void main() {
expect(groupExists, false);
});
// Verifies that updateGroupName correctly updates only the name field.
test('Updating a group name works correcly', () async {
await database.groupDao.addGroup(group: testGroup1);
const newGroupName = 'new group name';
await database.groupDao.updateGroupname(
await database.groupDao.updateGroupName(
groupId: testGroup1.id,
newName: newGroupName,
);
@@ -156,6 +163,7 @@ void main() {
expect(result.name, newGroupName);
});
// Verifies that getGroupCount returns correct count through add/delete operations.
test('Getting the group count works correctly', () async {
final initialCount = await database.groupDao.getGroupCount();
expect(initialCount, 0);
@@ -173,5 +181,189 @@ void main() {
final finalCount = await database.groupDao.getGroupCount();
expect(finalCount, 0);
});
// Verifies that getAllGroups returns an empty list when no groups exist.
test('getAllGroups returns empty list when no groups exist', () async {
final allGroups = await database.groupDao.getAllGroups();
expect(allGroups, isEmpty);
});
// Verifies that getGroupById throws StateError for non-existent group ID.
test('getGroupById throws exception for non-existent group', () async {
expect(
() => database.groupDao.getGroupById(groupId: 'non-existent-id'),
throwsA(isA<StateError>()),
);
});
// Verifies that addGroup returns false when trying to add a duplicate group.
test('addGroup returns false when group already exists', () async {
final firstAdd = await database.groupDao.addGroup(group: testGroup1);
expect(firstAdd, true);
final secondAdd = await database.groupDao.addGroup(group: testGroup1);
expect(secondAdd, false);
final allGroups = await database.groupDao.getAllGroups();
expect(allGroups.length, 1);
});
// Verifies that addGroupsAsList handles an empty list without errors.
test('addGroupsAsList handles empty list correctly', () async {
await database.groupDao.addGroupsAsList(groups: []);
final allGroups = await database.groupDao.getAllGroups();
expect(allGroups.length, 0);
});
// Verifies that deleteGroup returns false for a non-existent group ID.
test('deleteGroup returns false for non-existent group', () async {
final deleted = await database.groupDao.deleteGroup(
groupId: 'non-existent-id',
);
expect(deleted, false);
});
// Verifies that updateGroupName returns false for a non-existent group ID.
test('updateGroupName returns false for non-existent group', () async {
final updated = await database.groupDao.updateGroupName(
groupId: 'non-existent-id',
newName: 'New Name',
);
expect(updated, false);
});
// Verifies that updateGroupDescription correctly updates the description field.
test('Updating a group description works correctly', () async {
await database.groupDao.addGroup(group: testGroup1);
const newDescription = 'This is a new description';
final updated = await database.groupDao.updateGroupDescription(
groupId: testGroup1.id,
newDescription: newDescription,
);
expect(updated, true);
final result = await database.groupDao.getGroupById(
groupId: testGroup1.id,
);
expect(result.description, newDescription);
});
// Verifies that updateGroupDescription can set the description to null.
test('updateGroupDescription can set description to null', () async {
final groupWithDescription = Group(
name: 'Group with description',
description: 'Initial description',
members: [testPlayer1],
);
await database.groupDao.addGroup(group: groupWithDescription);
final updated = await database.groupDao.updateGroupDescription(
groupId: groupWithDescription.id,
newDescription: null,
);
expect(updated, true);
final result = await database.groupDao.getGroupById(
groupId: groupWithDescription.id,
);
expect(result.description, isNull);
});
// Verifies that updateGroupDescription returns false for a non-existent group.
test('updateGroupDescription returns false for non-existent group',
() async {
final updated = await database.groupDao.updateGroupDescription(
groupId: 'non-existent-id',
newDescription: 'New Description',
);
expect(updated, false);
});
// Verifies that deleteAllGroups removes all groups from the database.
test('deleteAllGroups removes all groups', () async {
await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup2],
);
final countBefore = await database.groupDao.getGroupCount();
expect(countBefore, 2);
final deleted = await database.groupDao.deleteAllGroups();
expect(deleted, true);
final countAfter = await database.groupDao.getGroupCount();
expect(countAfter, 0);
});
// Verifies that deleteAllGroups returns false when no groups exist.
test('deleteAllGroups returns false when no groups exist', () async {
final deleted = await database.groupDao.deleteAllGroups();
expect(deleted, false);
});
// Verifies that groups with special characters (quotes, emojis) are stored correctly.
test('Group with special characters in name is stored correctly', () async {
final specialGroup = Group(
name: 'Group\'s & "Special" <Name>',
description: 'Description with émojis 🎮🎲',
members: [testPlayer1],
);
await database.groupDao.addGroup(group: specialGroup);
final fetchedGroup = await database.groupDao.getGroupById(
groupId: specialGroup.id,
);
expect(fetchedGroup.name, 'Group\'s & "Special" <Name>');
expect(fetchedGroup.description, 'Description with émojis 🎮🎲');
});
// Verifies that a group with an empty members list can be stored and retrieved.
test('Group with empty members list is stored correctly', () async {
final emptyGroup = Group(
name: 'Empty Group',
members: [],
);
await database.groupDao.addGroup(group: emptyGroup);
final fetchedGroup = await database.groupDao.getGroupById(
groupId: emptyGroup.id,
);
expect(fetchedGroup.name, 'Empty Group');
expect(fetchedGroup.members, isEmpty);
});
// Verifies that multiple sequential updates to the same group work correctly.
test('Multiple updates to the same group work correctly', () async {
await database.groupDao.addGroup(group: testGroup1);
await database.groupDao.updateGroupName(
groupId: testGroup1.id,
newName: 'Updated Name',
);
await database.groupDao.updateGroupDescription(
groupId: testGroup1.id,
newDescription: 'Updated Description',
);
final updatedGroup = await database.groupDao.getGroupById(
groupId: testGroup1.id,
);
expect(updatedGroup.name, 'Updated Name');
expect(updatedGroup.description, 'Updated Description');
expect(updatedGroup.members.length, testGroup1.members.length);
});
// Verifies that addGroupsAsList with duplicate groups only adds unique ones.
test('addGroupsAsList with duplicate groups only adds once', () async {
await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup1, testGroup1],
);
final allGroups = await database.groupDao.getAllGroups();
expect(allGroups.length, 1);
});
});
}

View File

@@ -0,0 +1,287 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
void main() {
late AppDatabase database;
late Player testPlayer1;
late Player testPlayer2;
late Player testPlayer3;
late Player testPlayer4;
late Player testPlayer5;
late Group testGroup1;
late Group testGroup2;
late Game testGame;
late Match testMatch1;
late Match testMatch2;
late Match testMatchOnlyPlayers;
late Match testMatchOnlyGroup;
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
final fakeClock = Clock(() => fixedDate);
setUp(() async {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
// Recommended for widget tests to avoid test errors.
closeStreamsSynchronously: true,
),
);
withClock(fakeClock, () {
testPlayer1 = Player(name: 'Alice');
testPlayer2 = Player(name: 'Bob');
testPlayer3 = Player(name: 'Charlie');
testPlayer4 = Player(name: 'Diana');
testPlayer5 = Player(name: 'Eve');
testGroup1 = Group(
name: 'Test Group 2',
members: [testPlayer1, testPlayer2, testPlayer3],
);
testGroup2 = Group(
name: 'Test Group 2',
members: [testPlayer4, testPlayer5],
);
testGame = Game(name: 'Test Game');
testMatch1 = Match(
name: 'First Test Match',
game: testGame,
group: testGroup1,
players: [testPlayer4, testPlayer5],
winner: testPlayer4,
);
testMatch2 = Match(
name: 'Second Test Match',
game: testGame,
group: testGroup2,
players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer2,
);
testMatchOnlyPlayers = Match(
name: 'Test Match with Players',
game: testGame,
players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer3,
);
testMatchOnlyGroup = Match(
name: 'Test Match with Group',
game: testGame,
group: testGroup2,
);
});
await database.playerDao.addPlayersAsList(
players: [
testPlayer1,
testPlayer2,
testPlayer3,
testPlayer4,
testPlayer5,
],
);
await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
await database.gameDao.addGame(game: testGame);
});
tearDown(() async {
await database.close();
});
group('Match Tests', () {
// Verifies that a single match can be added and retrieved with all fields, group, and players intact.
test('Adding and fetching single match works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final result = await database.matchDao.getMatchById(
matchId: testMatch1.id,
);
expect(result.id, testMatch1.id);
expect(result.name, testMatch1.name);
expect(result.createdAt, testMatch1.createdAt);
if (result.group != null) {
expect(result.group!.members.length, testGroup1.members.length);
for (int i = 0; i < testGroup1.members.length; i++) {
expect(result.group!.members[i].id, testGroup1.members[i].id);
expect(result.group!.members[i].name, testGroup1.members[i].name);
}
} else {
fail('Group is null');
}
if (result.players != null) {
expect(result.players!.length, testMatch1.players!.length);
for (int i = 0; i < testMatch1.players!.length; i++) {
expect(result.players![i].id, testMatch1.players![i].id);
expect(result.players![i].name, testMatch1.players![i].name);
expect(
result.players![i].createdAt,
testMatch1.players![i].createdAt,
);
}
} else {
fail('Players is null');
}
});
// Verifies that multiple matches can be added and retrieved with correct groups and players.
test('Adding and fetching multiple matches works correctly', () async {
await database.matchDao.addMatchAsList(
matches: [
testMatch1,
testMatch2,
testMatchOnlyGroup,
testMatchOnlyPlayers,
],
);
final allMatches = await database.matchDao.getAllMatches();
expect(allMatches.length, 4);
final testMatches = {
testMatch1.id: testMatch1,
testMatch2.id: testMatch2,
testMatchOnlyGroup.id: testMatchOnlyGroup,
testMatchOnlyPlayers.id: testMatchOnlyPlayers,
};
for (final match in allMatches) {
final testMatch = testMatches[match.id]!;
// Match-Checks
expect(match.id, testMatch.id);
expect(match.name, testMatch.name);
expect(match.createdAt, testMatch.createdAt);
// Group-Checks
if (testMatch.group != null) {
expect(match.group!.id, testMatch.group!.id);
expect(match.group!.name, testMatch.group!.name);
expect(match.group!.createdAt, testMatch.group!.createdAt);
// Group Members-Checks
expect(match.group!.members.length, testMatch.group!.members.length);
for (int i = 0; i < testMatch.group!.members.length; i++) {
expect(match.group!.members[i].id, testMatch.group!.members[i].id);
expect(
match.group!.members[i].name,
testMatch.group!.members[i].name,
);
expect(
match.group!.members[i].createdAt,
testMatch.group!.members[i].createdAt,
);
}
} else {
expect(match.group, null);
}
// Players-Checks
if (testMatch.players != null) {
expect(match.players!.length, testMatch.players!.length);
for (int i = 0; i < testMatch.players!.length; i++) {
expect(match.players![i].id, testMatch.players![i].id);
expect(match.players![i].name, testMatch.players![i].name);
expect(
match.players![i].createdAt,
testMatch.players![i].createdAt,
);
}
} else {
expect(match.players, null);
}
}
});
// Verifies that adding the same match twice does not create duplicates.
test('Adding the same match twice does not create duplicates', () async {
await database.matchDao.addMatch(match: testMatch1);
await database.matchDao.addMatch(match: testMatch1);
final matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 1);
});
// Verifies that matchExists returns correct boolean based on match presence.
test('Match existence check works correctly', () async {
var matchExists = await database.matchDao.matchExists(
matchId: testMatch1.id,
);
expect(matchExists, false);
await database.matchDao.addMatch(match: testMatch1);
matchExists = await database.matchDao.matchExists(matchId: testMatch1.id);
expect(matchExists, true);
});
// Verifies that deleteMatch removes the match and returns true.
test('Deleting a match works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final matchDeleted = await database.matchDao.deleteMatch(
matchId: testMatch1.id,
);
expect(matchDeleted, true);
final matchExists = await database.matchDao.matchExists(
matchId: testMatch1.id,
);
expect(matchExists, false);
});
// Verifies that getMatchCount returns correct count through add/delete operations.
test('Getting the match count works correctly', () async {
var matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 0);
await database.matchDao.addMatch(match: testMatch1);
matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 1);
await database.matchDao.addMatch(match: testMatch2);
matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 2);
await database.matchDao.deleteMatch(matchId: testMatch1.id);
matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 1);
await database.matchDao.deleteMatch(matchId: testMatch2.id);
matchCount = await database.matchDao.getMatchCount();
expect(matchCount, 0);
});
// Verifies that updateMatchName correctly updates only the name field.
test('Renaming a match works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
var fetchedMatch = await database.matchDao.getMatchById(
matchId: testMatch1.id,
);
expect(fetchedMatch.name, testMatch1.name);
const newName = 'Updated Match Name';
await database.matchDao.updateMatchName(
matchId: testMatch1.id,
newName: newName,
);
fetchedMatch = await database.matchDao.getMatchById(
matchId: testMatch1.id,
);
expect(fetchedMatch.name, newName);
});
});
}

View File

@@ -12,7 +12,7 @@ void main() {
late Player testPlayer2;
late Player testPlayer3;
late Player testPlayer4;
late Group testgroup;
late Group testGroup;
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
final fakeClock = Clock(() => fixedDate);
@@ -30,7 +30,7 @@ void main() {
testPlayer2 = Player(name: 'Bob');
testPlayer3 = Player(name: 'Charlie');
testPlayer4 = Player(name: 'Diana');
testgroup = Group(
testGroup = Group(
name: 'Test Group',
members: [testPlayer1, testPlayer2, testPlayer3],
);
@@ -41,63 +41,271 @@ void main() {
});
group('Player-Group Tests', () {
/// No need to test if group has players since the members attribute is
/// not nullable
// Verifies that a player can be added to an existing group and isPlayerInGroup returns true.
test('Adding a player to a group works correctly', () async {
await database.groupDao.addGroup(group: testgroup);
await database.groupDao.addGroup(group: testGroup);
await database.playerDao.addPlayer(player: testPlayer4);
await database.playerGroupDao.addPlayerToGroup(
groupId: testgroup.id,
groupId: testGroup.id,
player: testPlayer4,
);
var playerAdded = await database.playerGroupDao.isPlayerInGroup(
groupId: testgroup.id,
groupId: testGroup.id,
playerId: testPlayer4.id,
);
expect(playerAdded, true);
playerAdded = await database.playerGroupDao.isPlayerInGroup(
groupId: testgroup.id,
groupId: testGroup.id,
playerId: '',
);
expect(playerAdded, false);
});
// Verifies that a player can be removed from a group and the group's member count decreases.
test('Removing player from group works correctly', () async {
await database.groupDao.addGroup(group: testgroup);
await database.groupDao.addGroup(group: testGroup);
final playerToRemove = testgroup.members[0];
final playerToRemove = testGroup.members[0];
final removed = await database.playerGroupDao.removePlayerFromGroup(
playerId: playerToRemove.id,
groupId: testgroup.id,
groupId: testGroup.id,
);
expect(removed, true);
final result = await database.groupDao.getGroupById(
groupId: testgroup.id,
groupId: testGroup.id,
);
expect(result.members.length, testgroup.members.length - 1);
expect(result.members.length, testGroup.members.length - 1);
final playerExists = result.members.any((p) => p.id == playerToRemove.id);
expect(playerExists, false);
});
// Verifies that getPlayersOfGroup returns all members of a group with correct data.
test('Retrieving players of a group works correctly', () async {
await database.groupDao.addGroup(group: testgroup);
await database.groupDao.addGroup(group: testGroup);
final players = await database.playerGroupDao.getPlayersOfGroup(
groupId: testgroup.id,
groupId: testGroup.id,
);
for (int i = 0; i < players.length; i++) {
expect(players[i].id, testgroup.members[i].id);
expect(players[i].name, testgroup.members[i].name);
expect(players[i].createdAt, testgroup.members[i].createdAt);
expect(players[i].id, testGroup.members[i].id);
expect(players[i].name, testGroup.members[i].name);
expect(players[i].createdAt, testGroup.members[i].createdAt);
}
});
// Verifies that isPlayerInGroup returns false for non-existent player.
test('isPlayerInGroup returns false for non-existent player', () async {
await database.groupDao.addGroup(group: testGroup);
final result = await database.playerGroupDao.isPlayerInGroup(
playerId: 'non-existent-player-id',
groupId: testGroup.id,
);
expect(result, false);
});
// Verifies that isPlayerInGroup returns false for non-existent group.
test('isPlayerInGroup returns false for non-existent group', () async {
await database.playerDao.addPlayer(player: testPlayer1);
final result = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id,
groupId: 'non-existent-group-id',
);
expect(result, false);
});
// Verifies that addPlayerToGroup returns false when player already in group.
test('addPlayerToGroup returns false when player already in group', () async {
await database.groupDao.addGroup(group: testGroup);
// testPlayer1 is already in testGroup via group creation
final result = await database.playerGroupDao.addPlayerToGroup(
player: testPlayer1,
groupId: testGroup.id,
);
expect(result, false);
});
// Verifies that addPlayerToGroup adds player to player table if not exists.
test('addPlayerToGroup adds player to player table if not exists', () async {
await database.groupDao.addGroup(group: testGroup);
// testPlayer4 is not in the database yet
var playerExists = await database.playerDao.playerExists(
playerId: testPlayer4.id,
);
expect(playerExists, false);
await database.playerGroupDao.addPlayerToGroup(
player: testPlayer4,
groupId: testGroup.id,
);
// Now player should exist in player table
playerExists = await database.playerDao.playerExists(
playerId: testPlayer4.id,
);
expect(playerExists, true);
});
// Verifies that removePlayerFromGroup returns false for non-existent player.
test('removePlayerFromGroup returns false for non-existent player', () async {
await database.groupDao.addGroup(group: testGroup);
final result = await database.playerGroupDao.removePlayerFromGroup(
playerId: 'non-existent-player-id',
groupId: testGroup.id,
);
expect(result, false);
});
// Verifies that removePlayerFromGroup returns false for non-existent group.
test('removePlayerFromGroup returns false for non-existent group', () async {
await database.playerDao.addPlayer(player: testPlayer1);
final result = await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: 'non-existent-group-id',
);
expect(result, false);
});
// Verifies that getPlayersOfGroup returns empty list for group with no members.
test('getPlayersOfGroup returns empty list for empty group', () async {
final emptyGroup = Group(name: 'Empty Group', members: []);
await database.groupDao.addGroup(group: emptyGroup);
final players = await database.playerGroupDao.getPlayersOfGroup(
groupId: emptyGroup.id,
);
expect(players, isEmpty);
});
// Verifies that getPlayersOfGroup returns empty list for non-existent group.
test('getPlayersOfGroup returns empty list for non-existent group', () async {
final players = await database.playerGroupDao.getPlayersOfGroup(
groupId: 'non-existent-group-id',
);
expect(players, isEmpty);
});
// Verifies that removing all players from a group leaves the group empty.
test('Removing all players from a group leaves group empty', () async {
await database.groupDao.addGroup(group: testGroup);
for (final player in testGroup.members) {
await database.playerGroupDao.removePlayerFromGroup(
playerId: player.id,
groupId: testGroup.id,
);
}
final players = await database.playerGroupDao.getPlayersOfGroup(
groupId: testGroup.id,
);
expect(players, isEmpty);
// Group should still exist
final groupExists = await database.groupDao.groupExists(
groupId: testGroup.id,
);
expect(groupExists, true);
});
// Verifies that a player can be in multiple groups.
test('Player can be in multiple groups', () async {
final secondGroup = Group(name: 'Second Group', members: []);
await database.groupDao.addGroup(group: testGroup);
await database.groupDao.addGroup(group: secondGroup);
// Add testPlayer1 to second group (already in testGroup)
await database.playerGroupDao.addPlayerToGroup(
player: testPlayer1,
groupId: secondGroup.id,
);
final inFirstGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
final inSecondGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id,
groupId: secondGroup.id,
);
expect(inFirstGroup, true);
expect(inSecondGroup, true);
});
// Verifies that removing player from one group doesn't affect other groups.
test('Removing player from one group does not affect other groups', () async {
final secondGroup = Group(name: 'Second Group', members: [testPlayer1]);
await database.groupDao.addGroup(group: testGroup);
await database.groupDao.addGroup(group: secondGroup);
// Remove testPlayer1 from testGroup
await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
final inFirstGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
final inSecondGroup = await database.playerGroupDao.isPlayerInGroup(
playerId: testPlayer1.id,
groupId: secondGroup.id,
);
expect(inFirstGroup, false);
expect(inSecondGroup, true);
});
// Verifies that addPlayerToGroup returns true on successful addition.
test('addPlayerToGroup returns true on successful addition', () async {
await database.groupDao.addGroup(group: testGroup);
await database.playerDao.addPlayer(player: testPlayer4);
final result = await database.playerGroupDao.addPlayerToGroup(
player: testPlayer4,
groupId: testGroup.id,
);
expect(result, true);
});
// Verifies that removing the same player twice returns false on second attempt.
test('Removing same player twice returns false on second attempt', () async {
await database.groupDao.addGroup(group: testGroup);
final firstRemoval = await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
expect(firstRemoval, true);
final secondRemoval = await database.playerGroupDao.removePlayerFromGroup(
playerId: testPlayer1.id,
groupId: testGroup.id,
);
expect(secondRemoval, false);
});
});
}

View File

@@ -1,11 +1,13 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' hide isNotNull;
import 'package:drift/drift.dart' hide isNotNull, isNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/data/dto/team.dart';
void main() {
late AppDatabase database;
@@ -15,10 +17,13 @@ void main() {
late Player testPlayer4;
late Player testPlayer5;
late Player testPlayer6;
late Group testgroup;
late Group testGroup;
late Game testGame;
late Match testMatchOnlyGroup;
late Match testMatchOnlyPlayers;
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
late Team testTeam1;
late Team testTeam2;
final fixedDate = DateTime(2025, 11, 19, 00, 11, 23);
final fakeClock = Clock(() => fixedDate);
setUp(() async {
@@ -37,18 +42,29 @@ void main() {
testPlayer4 = Player(name: 'Diana');
testPlayer5 = Player(name: 'Eve');
testPlayer6 = Player(name: 'Frank');
testgroup = Group(
testGroup = Group(
name: 'Test Group',
members: [testPlayer1, testPlayer2, testPlayer3],
);
testGame = Game(name: 'Test Game');
testMatchOnlyGroup = Match(
name: 'Test Match with Group',
group: testgroup,
game: testGame,
group: testGroup,
);
testMatchOnlyPlayers = Match(
name: 'Test Match with Players',
game: testGame,
players: [testPlayer4, testPlayer5, testPlayer6],
);
testTeam1 = Team(
name: 'Team Alpha',
members: [testPlayer1, testPlayer2],
);
testTeam2 = Team(
name: 'Team Beta',
members: [testPlayer3, testPlayer4],
);
});
await database.playerDao.addPlayersAsList(
players: [
@@ -60,13 +76,16 @@ void main() {
testPlayer6,
],
);
await database.groupDao.addGroup(group: testgroup);
await database.groupDao.addGroup(group: testGroup);
await database.gameDao.addGame(game: testGame);
});
tearDown(() async {
await database.close();
});
group('Player-Match Tests', () {
// Verifies that matchHasPlayers returns false initially and true after adding a player.
test('Match has player works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerDao.addPlayer(player: testPlayer1);
@@ -89,6 +108,7 @@ void main() {
expect(matchHasPlayers, true);
});
// Verifies that a player can be added to a match and isPlayerInMatch returns true.
test('Adding a player to a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerDao.addPlayer(player: testPlayer5);
@@ -112,6 +132,7 @@ void main() {
expect(playerAdded, false);
});
// Verifies that a player can be removed from a match and the player count decreases.
test('Removing player from match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
@@ -134,6 +155,7 @@ void main() {
expect(playerExists, false);
});
// Verifies that getPlayersOfMatch returns all players of a match with correct data.
test('Retrieving players of a match works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final players = await database.playerMatchDao.getPlayersOfMatch(
@@ -154,7 +176,8 @@ void main() {
}
});
test('Updating the match players works coreclty', () async {
// Verifies that updatePlayersFromMatch replaces all existing players with new ones.
test('Updating the match players works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final newPlayers = [testPlayer1, testPlayer2, testPlayer4];
@@ -197,12 +220,13 @@ void main() {
}
});
// Verifies that the same player can be added to multiple different matches.
test(
'Adding the same player to seperate matches works correctly',
'Adding the same player to separate matches works correctly',
() async {
final playersList = [testPlayer1, testPlayer2, testPlayer3];
final match1 = Match(name: 'Match 1', players: playersList);
final match2 = Match(name: 'Match 2', players: playersList);
final match1 = Match(name: 'Match 1', game: testGame, players: playersList);
final match2 = Match(name: 'Match 2', game: testGame, players: playersList);
await Future.wait([
database.matchDao.addMatch(match: match1),
@@ -233,5 +257,628 @@ void main() {
);
},
);
// Verifies that getPlayersOfMatch returns null for a non-existent match.
test('getPlayersOfMatch returns null for non-existent match', () async {
final players = await database.playerMatchDao.getPlayersOfMatch(
matchId: 'non-existent-match-id',
);
expect(players, isNull);
});
// Verifies that adding a player with initial score works correctly.
test('Adding player with initial score works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
score: 100,
);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
expect(score, 100);
});
// Verifies that getPlayerScore returns the correct score.
test('getPlayerScore returns correct score', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// Default score should be 0 when added through match
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 0);
});
// Verifies that getPlayerScore returns null for non-existent player-match combination.
test('getPlayerScore returns null for non-existent player in match', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: 'non-existent-player-id',
);
expect(score, isNull);
});
// Verifies that updatePlayerScore updates the score correctly.
test('updatePlayerScore updates score correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 50,
);
expect(updated, true);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 50);
});
// Verifies that updatePlayerScore returns false for non-existent player-match.
test('updatePlayerScore returns false for non-existent player-match', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: 'non-existent-player-id',
newScore: 50,
);
expect(updated, false);
});
// Verifies that adding a player with teamId works correctly.
test('Adding player with teamId works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
teamId: testTeam1.id,
);
final playersInTeam = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam1.id,
);
expect(playersInTeam.length, 1);
expect(playersInTeam[0].id, testPlayer1.id);
});
// Verifies that updatePlayerTeam updates the team correctly.
test('updatePlayerTeam updates team correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
await database.teamDao.addTeam(team: testTeam2);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
teamId: testTeam1.id,
);
// Update player's team
final updated = await database.playerMatchDao.updatePlayerTeam(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
teamId: testTeam2.id,
);
expect(updated, true);
// Verify player is now in testTeam2
final playersInTeam2 = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam2.id,
);
expect(playersInTeam2.length, 1);
expect(playersInTeam2[0].id, testPlayer1.id);
// Verify player is no longer in testTeam1
final playersInTeam1 = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam1.id,
);
expect(playersInTeam1.isEmpty, true);
});
// Verifies that updatePlayerTeam can set team to null.
test('updatePlayerTeam can remove player from team', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
teamId: testTeam1.id,
);
// Remove player from team by setting teamId to null
final updated = await database.playerMatchDao.updatePlayerTeam(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
teamId: null,
);
expect(updated, true);
final playersInTeam = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam1.id,
);
expect(playersInTeam.isEmpty, true);
});
// Verifies that updatePlayerTeam returns false for non-existent player-match.
test('updatePlayerTeam returns false for non-existent player-match', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
final updated = await database.playerMatchDao.updatePlayerTeam(
matchId: testMatchOnlyGroup.id,
playerId: 'non-existent-player-id',
teamId: testTeam1.id,
);
expect(updated, false);
});
// Verifies that getPlayersInTeam returns empty list for non-existent team.
test('getPlayersInTeam returns empty list for non-existent team', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final players = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyPlayers.id,
teamId: 'non-existent-team-id',
);
expect(players.isEmpty, true);
});
// Verifies that getPlayersInTeam returns all players of a team.
test('getPlayersInTeam returns all players of a team', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
teamId: testTeam1.id,
);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer2.id,
teamId: testTeam1.id,
);
final playersInTeam = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam1.id,
);
expect(playersInTeam.length, 2);
final playerIds = playersInTeam.map((p) => p.id).toSet();
expect(playerIds.contains(testPlayer1.id), true);
expect(playerIds.contains(testPlayer2.id), true);
});
// Verifies that removePlayerFromMatch returns false for non-existent player.
test('removePlayerFromMatch returns false for non-existent player', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final removed = await database.playerMatchDao.removePlayerFromMatch(
playerId: 'non-existent-player-id',
matchId: testMatchOnlyPlayers.id,
);
expect(removed, false);
});
// Verifies that adding the same player twice to the same match is ignored.
test('Adding same player twice to same match is ignored', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
score: 10,
);
// Try to add the same player again with different score
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
score: 100,
);
// Score should still be 10 because insert was ignored
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
expect(score, 10);
// Verify player count is still 1
final players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyGroup.id,
);
expect(players?.length, 1);
});
// Verifies that updatePlayersFromMatch with empty list removes all players.
test('updatePlayersFromMatch with empty list removes all players', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// Verify players exist initially
var players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id,
);
expect(players?.length, 3);
// Update with empty list
await database.playerMatchDao.updatePlayersFromMatch(
matchId: testMatchOnlyPlayers.id,
newPlayer: [],
);
// Verify all players are removed
players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id,
);
expect(players, isNull);
});
// Verifies that updatePlayersFromMatch with same players makes no changes.
test('updatePlayersFromMatch with same players makes no changes', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final originalPlayers = [testPlayer4, testPlayer5, testPlayer6];
await database.playerMatchDao.updatePlayersFromMatch(
matchId: testMatchOnlyPlayers.id,
newPlayer: originalPlayers,
);
final players = await database.playerMatchDao.getPlayersOfMatch(
matchId: testMatchOnlyPlayers.id,
);
expect(players?.length, originalPlayers.length);
final playerIds = players!.map((p) => p.id).toSet();
for (final originalPlayer in originalPlayers) {
expect(playerIds.contains(originalPlayer.id), true);
}
});
// Verifies that matchHasPlayers returns false for non-existent match.
test('matchHasPlayers returns false for non-existent match', () async {
final hasPlayers = await database.playerMatchDao.matchHasPlayers(
matchId: 'non-existent-match-id',
);
expect(hasPlayers, false);
});
// Verifies that isPlayerInMatch returns false for non-existent match.
test('isPlayerInMatch returns false for non-existent match', () async {
final isInMatch = await database.playerMatchDao.isPlayerInMatch(
matchId: 'non-existent-match-id',
playerId: testPlayer1.id,
);
expect(isInMatch, false);
});
// Verifies that updatePlayersFromMatch preserves scores for existing players.
test('updatePlayersFromMatch only modifies player associations', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// Update score for existing player
await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 75,
);
// Update players, keeping testPlayer4 and adding testPlayer1
await database.playerMatchDao.updatePlayersFromMatch(
matchId: testMatchOnlyPlayers.id,
newPlayer: [testPlayer4, testPlayer1],
);
// Verify testPlayer4's score is preserved
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 75);
// Verify testPlayer1 was added with default score
final newPlayerScore = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer1.id,
);
expect(newPlayerScore, 0);
});
// Verifies that adding a player with both score and teamId works correctly.
test('Adding player with score and teamId works correctly', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
teamId: testTeam1.id,
score: 150,
);
// Verify score
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
expect(score, 150);
// Verify team assignment
final playersInTeam = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam1.id,
);
expect(playersInTeam.length, 1);
expect(playersInTeam[0].id, testPlayer1.id);
});
// Verifies that updating score with negative value works.
test('updatePlayerScore with negative score works', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: -10,
);
expect(updated, true);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, -10);
});
// Verifies that updating score with zero value works.
test('updatePlayerScore with zero score works', () async {
await database.matchDao.addMatch(match: testMatchOnlyPlayers);
// First set a non-zero score
await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 100,
);
// Then update to zero
final updated = await database.playerMatchDao.updatePlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
newScore: 0,
);
expect(updated, true);
final score = await database.playerMatchDao.getPlayerScore(
matchId: testMatchOnlyPlayers.id,
playerId: testPlayer4.id,
);
expect(score, 0);
});
// Verifies that getPlayersInTeam returns empty list for non-existent match.
test('getPlayersInTeam returns empty list for non-existent match', () async {
await database.teamDao.addTeam(team: testTeam1);
final players = await database.playerMatchDao.getPlayersInTeam(
matchId: 'non-existent-match-id',
teamId: testTeam1.id,
);
expect(players.isEmpty, true);
});
// Verifies that players in different teams within the same match are returned correctly.
test('Players in different teams within same match are separate', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
await database.teamDao.addTeam(team: testTeam2);
// Add players to different teams
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
teamId: testTeam1.id,
);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer2.id,
teamId: testTeam1.id,
);
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer3.id,
teamId: testTeam2.id,
);
// Verify team 1 players
final playersInTeam1 = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam1.id,
);
expect(playersInTeam1.length, 2);
final team1Ids = playersInTeam1.map((p) => p.id).toSet();
expect(team1Ids.contains(testPlayer1.id), true);
expect(team1Ids.contains(testPlayer2.id), true);
expect(team1Ids.contains(testPlayer3.id), false);
// Verify team 2 players
final playersInTeam2 = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam2.id,
);
expect(playersInTeam2.length, 1);
expect(playersInTeam2[0].id, testPlayer3.id);
});
// Verifies that removePlayerFromMatch does not affect other matches.
test('removePlayerFromMatch does not affect other matches', () async {
final playersList = [testPlayer1, testPlayer2];
final match1 = Match(name: 'Match 1', game: testGame, players: playersList);
final match2 = Match(name: 'Match 2', game: testGame, players: playersList);
await Future.wait([
database.matchDao.addMatch(match: match1),
database.matchDao.addMatch(match: match2),
]);
// Remove player from match1
final removed = await database.playerMatchDao.removePlayerFromMatch(
playerId: testPlayer1.id,
matchId: match1.id,
);
expect(removed, true);
// Verify player is removed from match1
final isInMatch1 = await database.playerMatchDao.isPlayerInMatch(
matchId: match1.id,
playerId: testPlayer1.id,
);
expect(isInMatch1, false);
// Verify player still exists in match2
final isInMatch2 = await database.playerMatchDao.isPlayerInMatch(
matchId: match2.id,
playerId: testPlayer1.id,
);
expect(isInMatch2, true);
});
// Verifies that updating scores for players in different matches are independent.
test('Player scores are independent across matches', () async {
final playersList = [testPlayer1];
final match1 = Match(name: 'Match 1', game: testGame, players: playersList);
final match2 = Match(name: 'Match 2', game: testGame, players: playersList);
await Future.wait([
database.matchDao.addMatch(match: match1),
database.matchDao.addMatch(match: match2),
]);
// Update score in match1
await database.playerMatchDao.updatePlayerScore(
matchId: match1.id,
playerId: testPlayer1.id,
newScore: 100,
);
// Update score in match2
await database.playerMatchDao.updatePlayerScore(
matchId: match2.id,
playerId: testPlayer1.id,
newScore: 50,
);
// Verify scores are independent
final scoreInMatch1 = await database.playerMatchDao.getPlayerScore(
matchId: match1.id,
playerId: testPlayer1.id,
);
final scoreInMatch2 = await database.playerMatchDao.getPlayerScore(
matchId: match2.id,
playerId: testPlayer1.id,
);
expect(scoreInMatch1, 100);
expect(scoreInMatch2, 50);
});
// Verifies that updatePlayersFromMatch on non-existent match fails with constraint error.
test('updatePlayersFromMatch on non-existent match fails with foreign key constraint', () async {
// Should throw due to foreign key constraint - match doesn't exist
await expectLater(
database.playerMatchDao.updatePlayersFromMatch(
matchId: 'non-existent-match-id',
newPlayer: [testPlayer1, testPlayer2],
),
throwsA(anything),
);
});
// Verifies that a player can be in a match without being assigned to a team.
test('Player can exist in match without team assignment', () async {
await database.matchDao.addMatch(match: testMatchOnlyGroup);
await database.teamDao.addTeam(team: testTeam1);
// Add player to match without team
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
// Add another player to match with team
await database.playerMatchDao.addPlayerToMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer2.id,
teamId: testTeam1.id,
);
// Verify both players are in the match
final isPlayer1InMatch = await database.playerMatchDao.isPlayerInMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer1.id,
);
final isPlayer2InMatch = await database.playerMatchDao.isPlayerInMatch(
matchId: testMatchOnlyGroup.id,
playerId: testPlayer2.id,
);
expect(isPlayer1InMatch, true);
expect(isPlayer2InMatch, true);
// Verify only player2 is in the team
final playersInTeam = await database.playerMatchDao.getPlayersInTeam(
matchId: testMatchOnlyGroup.id,
teamId: testTeam1.id,
);
expect(playersInTeam.length, 1);
expect(playersInTeam[0].id, testPlayer2.id);
});
});
}

View File

@@ -1,5 +1,5 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
@@ -35,6 +35,8 @@ void main() {
});
group('Player Tests', () {
// Verifies that players can be added and retrieved with all fields intact.
test('Adding and fetching single player works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
@@ -55,6 +57,7 @@ void main() {
expect(fetchedPlayer2.createdAt, testPlayer2.createdAt);
});
// Verifies that multiple players can be added at once and retrieved correctly.
test('Adding and fetching multiple players works correctly', () async {
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4],
@@ -63,7 +66,7 @@ void main() {
final allPlayers = await database.playerDao.getAllPlayers();
expect(allPlayers.length, 4);
// Map for connencting fetched players with expected players
// Map for connecting fetched players with expected players
final testPlayers = {
testPlayer1.id: testPlayer1,
testPlayer2.id: testPlayer2,
@@ -80,6 +83,7 @@ void main() {
}
});
// Verifies that adding the same player twice does not create duplicates.
test('Adding the same player twice does not create duplicates', () async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer1);
@@ -88,6 +92,7 @@ void main() {
expect(allPlayers.length, 1);
});
// Verifies that playerExists returns correct boolean based on player presence.
test('Player existence check works correctly', () async {
var playerExists = await database.playerDao.playerExists(
playerId: testPlayer1.id,
@@ -102,6 +107,7 @@ void main() {
expect(playerExists, true);
});
// Verifies that deletePlayer removes the player and returns true.
test('Deleting a player works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1);
final playerDeleted = await database.playerDao.deletePlayer(
@@ -115,12 +121,13 @@ void main() {
expect(playerExists, false);
});
test('Updating a player name works correcly', () async {
// Verifies that updatePlayerName correctly updates only the name field.
test('Updating a player name works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1);
const newPlayerName = 'new player name';
await database.playerDao.updatePlayername(
await database.playerDao.updatePlayerName(
playerId: testPlayer1.id,
newName: newPlayerName,
);
@@ -131,6 +138,7 @@ void main() {
expect(result.name, newPlayerName);
});
// Verifies that getPlayerCount returns correct count through add/delete operations.
test('Getting the player count works correctly', () async {
var playerCount = await database.playerDao.getPlayerCount();
expect(playerCount, 0);
@@ -155,5 +163,215 @@ void main() {
playerCount = await database.playerDao.getPlayerCount();
expect(playerCount, 0);
});
// Verifies that getAllPlayers returns an empty list when no players exist.
test('getAllPlayers returns empty list when no players exist', () async {
final allPlayers = await database.playerDao.getAllPlayers();
expect(allPlayers, isEmpty);
});
// Verifies that getPlayerById returns the correct player.
test('getPlayerById returns correct player', () async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
final fetchedPlayer = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(fetchedPlayer.id, testPlayer1.id);
expect(fetchedPlayer.name, testPlayer1.name);
expect(fetchedPlayer.createdAt, testPlayer1.createdAt);
expect(fetchedPlayer.description, testPlayer1.description);
});
// Verifies that getPlayerById throws StateError for non-existent player ID.
test('getPlayerById throws exception for non-existent player', () async {
expect(
() => database.playerDao.getPlayerById(playerId: 'non-existent-id'),
throwsA(isA<StateError>()),
);
});
// Verifies that addPlayer returns false when trying to add a duplicate player.
test('addPlayer returns false when player already exists', () async {
final firstAdd = await database.playerDao.addPlayer(player: testPlayer1);
expect(firstAdd, true);
final secondAdd = await database.playerDao.addPlayer(player: testPlayer1);
expect(secondAdd, false);
});
// Verifies that addPlayersAsList handles empty list correctly.
test('addPlayersAsList handles empty list correctly', () async {
final result = await database.playerDao.addPlayersAsList(players: []);
expect(result, false);
final allPlayers = await database.playerDao.getAllPlayers();
expect(allPlayers, isEmpty);
});
// Verifies that addPlayersAsList ignores duplicate player IDs.
test('addPlayersAsList with duplicate IDs ignores duplicates', () async {
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer1, testPlayer2],
);
final allPlayers = await database.playerDao.getAllPlayers();
expect(allPlayers.length, 2);
});
// Verifies that deletePlayer returns false for non-existent player.
test('deletePlayer returns false for non-existent player', () async {
final result = await database.playerDao.deletePlayer(
playerId: 'non-existent-id',
);
expect(result, false);
});
// Verifies that updatePlayerName does nothing for non-existent player (no exception).
test('updatePlayerName does nothing for non-existent player', () async {
// Should not throw, just do nothing
await database.playerDao.updatePlayerName(
playerId: 'non-existent-id',
newName: 'New Name',
);
final allPlayers = await database.playerDao.getAllPlayers();
expect(allPlayers, isEmpty);
});
// Verifies that deleteAllPlayers removes all players.
test('deleteAllPlayers removes all players', () async {
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2, testPlayer3],
);
var playerCount = await database.playerDao.getPlayerCount();
expect(playerCount, 3);
final result = await database.playerDao.deleteAllPlayers();
expect(result, true);
playerCount = await database.playerDao.getPlayerCount();
expect(playerCount, 0);
});
// Verifies that deleteAllPlayers returns false when no players exist.
test('deleteAllPlayers returns false when no players exist', () async {
final result = await database.playerDao.deleteAllPlayers();
expect(result, false);
});
// Verifies that a player with special characters in name is stored correctly.
test('Player with special characters in name is stored correctly', () async {
final specialPlayer = Player(name: 'Test!@#\$%^&*()_+-=[]{}|;\':",.<>?/`~');
await database.playerDao.addPlayer(player: specialPlayer);
final fetchedPlayer = await database.playerDao.getPlayerById(
playerId: specialPlayer.id,
);
expect(fetchedPlayer.name, specialPlayer.name);
});
// Verifies that a player with description is stored correctly.
test('Player with description is stored correctly', () async {
final playerWithDescription = Player(
name: 'Described Player',
description: 'This is a test description',
);
await database.playerDao.addPlayer(player: playerWithDescription);
final fetchedPlayer = await database.playerDao.getPlayerById(
playerId: playerWithDescription.id,
);
expect(fetchedPlayer.name, playerWithDescription.name);
expect(fetchedPlayer.description, playerWithDescription.description);
});
// Verifies that a player with null description is stored correctly.
test('Player with null description is stored correctly', () async {
final playerWithoutDescription = Player(name: 'No Description Player');
await database.playerDao.addPlayer(player: playerWithoutDescription);
final fetchedPlayer = await database.playerDao.getPlayerById(
playerId: playerWithoutDescription.id,
);
expect(fetchedPlayer.description, isNull);
});
// Verifies that multiple updates to the same player work correctly.
test('Multiple updates to the same player work correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.updatePlayerName(
playerId: testPlayer1.id,
newName: 'First Update',
);
var fetchedPlayer = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(fetchedPlayer.name, 'First Update');
await database.playerDao.updatePlayerName(
playerId: testPlayer1.id,
newName: 'Second Update',
);
fetchedPlayer = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(fetchedPlayer.name, 'Second Update');
await database.playerDao.updatePlayerName(
playerId: testPlayer1.id,
newName: 'Third Update',
);
fetchedPlayer = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(fetchedPlayer.name, 'Third Update');
});
// Verifies that a player with empty string name is stored correctly.
test('Player with empty string name is stored correctly', () async {
final emptyNamePlayer = Player(name: '');
await database.playerDao.addPlayer(player: emptyNamePlayer);
final fetchedPlayer = await database.playerDao.getPlayerById(
playerId: emptyNamePlayer.id,
);
expect(fetchedPlayer.name, '');
});
// Verifies that a player with very long name is stored correctly.
test('Player with very long name is stored correctly', () async {
final longName = 'A' * 1000;
final longNamePlayer = Player(name: longName);
await database.playerDao.addPlayer(player: longNamePlayer);
final fetchedPlayer = await database.playerDao.getPlayerById(
playerId: longNamePlayer.id,
);
expect(fetchedPlayer.name, longName);
});
// Verifies that addPlayer returns true on first add.
test('addPlayer returns true when player is added successfully', () async {
final result = await database.playerDao.addPlayer(player: testPlayer1);
expect(result, true);
final playerExists = await database.playerDao.playerExists(
playerId: testPlayer1.id,
);
expect(playerExists, true);
});
});
}

View File

@@ -0,0 +1,736 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' hide isNull, isNotNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
void main() {
late AppDatabase database;
late Player testPlayer1;
late Player testPlayer2;
late Player testPlayer3;
late Game testGame;
late Match testMatch1;
late Match testMatch2;
final fixedDate = DateTime(2025, 11, 19, 00, 11, 23);
final fakeClock = Clock(() => fixedDate);
setUp(() async {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
// Recommended for widget tests to avoid test errors.
closeStreamsSynchronously: true,
),
);
withClock(fakeClock, () {
testPlayer1 = Player(name: 'Alice');
testPlayer2 = Player(name: 'Bob');
testPlayer3 = Player(name: 'Charlie');
testGame = Game(name: 'Test Game');
testMatch1 = Match(
name: 'Test Match 1',
game: testGame,
players: [testPlayer1, testPlayer2],
);
testMatch2 = Match(
name: 'Test Match 2',
game: testGame,
players: [testPlayer2, testPlayer3],
);
});
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2, testPlayer3],
);
await database.gameDao.addGame(game: testGame);
await database.matchDao.addMatch(match: testMatch1);
await database.matchDao.addMatch(match: testMatch2);
});
tearDown(() async {
await database.close();
});
group('Score Tests', () {
// Verifies that a score can be added and retrieved with all fields intact.
test('Adding and fetching a score works correctly', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
final score = await database.scoreDao.getScoreForRound(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
);
expect(score, isNotNull);
expect(score!.playerId, testPlayer1.id);
expect(score.matchId, testMatch1.id);
expect(score.roundNumber, 1);
expect(score.score, 10);
expect(score.change, 10);
});
// Verifies that getScoresForMatch returns all scores for a given match.
test('Getting scores for a match works correctly', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer2.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 20,
change: 20,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 2,
score: 25,
change: 15,
);
final scores = await database.scoreDao.getScoresForMatch(
matchId: testMatch1.id,
);
expect(scores.length, 3);
});
// Verifies that getPlayerScoresInMatch returns all scores for a player in a match, ordered by round.
test('Getting player scores in a match works correctly', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 2,
score: 25,
change: 15,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 3,
score: 30,
change: 5,
);
final playerScores = await database.scoreDao.getPlayerScoresInMatch(
playerId: testPlayer1.id,
matchId: testMatch1.id,
);
expect(playerScores.length, 3);
expect(playerScores[0].roundNumber, 1);
expect(playerScores[1].roundNumber, 2);
expect(playerScores[2].roundNumber, 3);
expect(playerScores[0].score, 10);
expect(playerScores[1].score, 25);
expect(playerScores[2].score, 30);
});
// Verifies that getScoreForRound returns null for a non-existent round number.
test('Getting score for a non-existent round returns null', () async {
final score = await database.scoreDao.getScoreForRound(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 999,
);
expect(score, isNull);
});
// Verifies that updateScore correctly updates the score and change values.
test('Updating a score works correctly', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
final updated = await database.scoreDao.updateScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
newScore: 50,
newChange: 40,
);
expect(updated, true);
final score = await database.scoreDao.getScoreForRound(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
);
expect(score, isNotNull);
expect(score!.score, 50);
expect(score.change, 40);
});
// Verifies that updateScore returns false for a non-existent score entry.
test('Updating a non-existent score returns false', () async {
final updated = await database.scoreDao.updateScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 999,
newScore: 50,
newChange: 40,
);
expect(updated, false);
});
// Verifies that deleteScore removes the score entry and returns true.
test('Deleting a score works correctly', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
final deleted = await database.scoreDao.deleteScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
);
expect(deleted, true);
final score = await database.scoreDao.getScoreForRound(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
);
expect(score, isNull);
});
// Verifies that deleteScore returns false for a non-existent score entry.
test('Deleting a non-existent score returns false', () async {
final deleted = await database.scoreDao.deleteScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 999,
);
expect(deleted, false);
});
// Verifies that deleteScoresForMatch removes all scores for a match but keeps other match scores.
test('Deleting scores for a match works correctly', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer2.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 20,
change: 20,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch2.id,
roundNumber: 1,
score: 15,
change: 15,
);
final deleted = await database.scoreDao.deleteScoresForMatch(
matchId: testMatch1.id,
);
expect(deleted, true);
final match1Scores = await database.scoreDao.getScoresForMatch(
matchId: testMatch1.id,
);
expect(match1Scores.length, 0);
final match2Scores = await database.scoreDao.getScoresForMatch(
matchId: testMatch2.id,
);
expect(match2Scores.length, 1);
});
// Verifies that deleteScoresForPlayer removes all scores for a player across all matches.
test('Deleting scores for a player works correctly', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch2.id,
roundNumber: 1,
score: 15,
change: 15,
);
await database.scoreDao.addScore(
playerId: testPlayer2.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 20,
change: 20,
);
final deleted = await database.scoreDao.deleteScoresForPlayer(
playerId: testPlayer1.id,
);
expect(deleted, true);
final player1Scores = await database.scoreDao.getPlayerScoresInMatch(
playerId: testPlayer1.id,
matchId: testMatch1.id,
);
expect(player1Scores.length, 0);
final player2Scores = await database.scoreDao.getPlayerScoresInMatch(
playerId: testPlayer2.id,
matchId: testMatch1.id,
);
expect(player2Scores.length, 1);
});
// Verifies that getLatestRoundNumber returns the highest round number for a match.
test('Getting latest round number works correctly', () async {
var latestRound = await database.scoreDao.getLatestRoundNumber(
matchId: testMatch1.id,
);
expect(latestRound, 0);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
latestRound = await database.scoreDao.getLatestRoundNumber(
matchId: testMatch1.id,
);
expect(latestRound, 1);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 5,
score: 50,
change: 40,
);
latestRound = await database.scoreDao.getLatestRoundNumber(
matchId: testMatch1.id,
);
expect(latestRound, 5);
});
// Verifies that getTotalScoreForPlayer returns the latest score (cumulative) for a player.
test('Getting total score for a player works correctly', () async {
var totalScore = await database.scoreDao.getTotalScoreForPlayer(
playerId: testPlayer1.id,
matchId: testMatch1.id,
);
expect(totalScore, 0);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 2,
score: 25,
change: 15,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 3,
score: 40,
change: 15,
);
totalScore = await database.scoreDao.getTotalScoreForPlayer(
playerId: testPlayer1.id,
matchId: testMatch1.id,
);
expect(totalScore, 40);
});
// Verifies that adding a score with the same player/match/round replaces the existing one.
test('Adding the same score twice replaces the existing one', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 99,
change: 99,
);
final score = await database.scoreDao.getScoreForRound(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
);
expect(score, isNotNull);
expect(score!.score, 99);
expect(score.change, 99);
});
// Verifies that getScoresForMatch returns empty list for match with no scores.
test('Getting scores for match with no scores returns empty list', () async {
final scores = await database.scoreDao.getScoresForMatch(
matchId: testMatch1.id,
);
expect(scores.isEmpty, true);
});
// Verifies that getPlayerScoresInMatch returns empty list when player has no scores.
test('Getting player scores with no scores returns empty list', () async {
final playerScores = await database.scoreDao.getPlayerScoresInMatch(
playerId: testPlayer1.id,
matchId: testMatch1.id,
);
expect(playerScores.isEmpty, true);
});
// Verifies that scores can have negative values.
test('Score can have negative values', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: -10,
change: -10,
);
final score = await database.scoreDao.getScoreForRound(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
);
expect(score, isNotNull);
expect(score!.score, -10);
expect(score.change, -10);
});
// Verifies that scores can have zero values.
test('Score can have zero values', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 0,
change: 0,
);
final score = await database.scoreDao.getScoreForRound(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
);
expect(score, isNotNull);
expect(score!.score, 0);
expect(score.change, 0);
});
// Verifies that very large round numbers are supported.
test('Score supports very large round numbers', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 999999,
score: 100,
change: 100,
);
final score = await database.scoreDao.getScoreForRound(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 999999,
);
expect(score, isNotNull);
expect(score!.roundNumber, 999999);
});
// Verifies that getLatestRoundNumber returns max correctly for non-consecutive rounds.
test('Getting latest round number with non-consecutive rounds', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 5,
score: 50,
change: 40,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 3,
score: 30,
change: 20,
);
final latestRound = await database.scoreDao.getLatestRoundNumber(
matchId: testMatch1.id,
);
expect(latestRound, 5);
});
// Verifies that deleteScoresForMatch returns false when no scores exist.
test('Deleting scores for empty match returns false', () async {
final deleted = await database.scoreDao.deleteScoresForMatch(
matchId: testMatch1.id,
);
expect(deleted, false);
});
// Verifies that deleteScoresForPlayer returns false when player has no scores.
test('Deleting scores for player with no scores returns false', () async {
final deleted = await database.scoreDao.deleteScoresForPlayer(
playerId: testPlayer1.id,
);
expect(deleted, false);
});
// Verifies that multiple players in same match can have independent score updates.
test('Multiple players in same match have independent scores', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer2.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 20,
change: 20,
);
await database.scoreDao.updateScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
newScore: 100,
newChange: 90,
);
final player1Score = await database.scoreDao.getScoreForRound(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
);
final player2Score = await database.scoreDao.getScoreForRound(
playerId: testPlayer2.id,
matchId: testMatch1.id,
roundNumber: 1,
);
expect(player1Score!.score, 100);
expect(player2Score!.score, 20);
});
// Verifies that scores are isolated across different matches.
test('Scores are isolated across different matches', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch2.id,
roundNumber: 1,
score: 50,
change: 50,
);
final match1Scores = await database.scoreDao.getPlayerScoresInMatch(
playerId: testPlayer1.id,
matchId: testMatch1.id,
);
final match2Scores = await database.scoreDao.getPlayerScoresInMatch(
playerId: testPlayer1.id,
matchId: testMatch2.id,
);
expect(match1Scores.length, 1);
expect(match2Scores.length, 1);
expect(match1Scores[0].score, 10);
expect(match2Scores[0].score, 50);
});
// Verifies that getTotalScoreForPlayer returns latest score across multiple rounds.
test('Total score for player returns latest cumulative score', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 2,
score: 25,
change: 25,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 3,
score: 50,
change: 25,
);
final totalScore = await database.scoreDao.getTotalScoreForPlayer(
playerId: testPlayer1.id,
matchId: testMatch1.id,
);
// Should return the highest round's score
expect(totalScore, 50);
});
// Verifies that updating one player's score doesn't affect another player's score in same round.
test('Updating one player score does not affect other players in same round', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer2.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 20,
change: 20,
);
await database.scoreDao.addScore(
playerId: testPlayer3.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 30,
change: 30,
);
await database.scoreDao.updateScore(
playerId: testPlayer2.id,
matchId: testMatch1.id,
roundNumber: 1,
newScore: 99,
newChange: 89,
);
final scores = await database.scoreDao.getScoresForMatch(
matchId: testMatch1.id,
);
expect(scores.length, 3);
expect(scores.where((s) => s.playerId == testPlayer1.id).first.score, 10);
expect(scores.where((s) => s.playerId == testPlayer2.id).first.score, 99);
expect(scores.where((s) => s.playerId == testPlayer3.id).first.score, 30);
});
// Verifies that deleting a player's scores only affects that specific player.
test('Deleting player scores only affects target player', () async {
await database.scoreDao.addScore(
playerId: testPlayer1.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 10,
change: 10,
);
await database.scoreDao.addScore(
playerId: testPlayer2.id,
matchId: testMatch1.id,
roundNumber: 1,
score: 20,
change: 20,
);
await database.scoreDao.deleteScoresForPlayer(
playerId: testPlayer1.id,
);
final match1Scores = await database.scoreDao.getScoresForMatch(
matchId: testMatch1.id,
);
expect(match1Scores.length, 1);
expect(match1Scores[0].playerId, testPlayer2.id);
});
});
}

View File

@@ -0,0 +1,526 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/data/dto/team.dart';
void main() {
late AppDatabase database;
late Player testPlayer1;
late Player testPlayer2;
late Player testPlayer3;
late Player testPlayer4;
late Team testTeam1;
late Team testTeam2;
late Team testTeam3;
late Game testGame1;
late Game testGame2;
final fixedDate = DateTime(2025, 11, 19, 00, 11, 23);
final fakeClock = Clock(() => fixedDate);
setUp(() async {
database = AppDatabase(
DatabaseConnection(
NativeDatabase.memory(),
// Recommended for widget tests to avoid test errors.
closeStreamsSynchronously: true,
),
);
withClock(fakeClock, () {
testPlayer1 = Player(name: 'Alice');
testPlayer2 = Player(name: 'Bob');
testPlayer3 = Player(name: 'Charlie');
testPlayer4 = Player(name: 'Diana');
testTeam1 = Team(
name: 'Team Alpha',
members: [testPlayer1, testPlayer2],
);
testTeam2 = Team(
name: 'Team Beta',
members: [testPlayer3, testPlayer4],
);
testTeam3 = Team(
name: 'Team Gamma',
members: [testPlayer1, testPlayer3],
);
testGame1 = Game(name: 'Game 1');
testGame2 = Game(name: 'Game 2');
});
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4],
);
await database.gameDao.addGame(game: testGame1);
await database.gameDao.addGame(game: testGame2);
});
tearDown(() async {
await database.close();
});
group('Team Tests', () {
// Verifies that a single team can be added and retrieved with all fields intact.
test('Adding and fetching a single team works correctly', () async {
final added = await database.teamDao.addTeam(team: testTeam1);
expect(added, true);
final fetchedTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id,
);
expect(fetchedTeam.id, testTeam1.id);
expect(fetchedTeam.name, testTeam1.name);
expect(fetchedTeam.createdAt, testTeam1.createdAt);
});
// Verifies that multiple teams can be added at once and retrieved correctly.
test('Adding and fetching multiple teams works correctly', () async {
await database.teamDao.addTeamsAsList(
teams: [testTeam1, testTeam2, testTeam3],
);
final allTeams = await database.teamDao.getAllTeams();
expect(allTeams.length, 3);
final testTeams = {
testTeam1.id: testTeam1,
testTeam2.id: testTeam2,
testTeam3.id: testTeam3,
};
for (final team in allTeams) {
final testTeam = testTeams[team.id]!;
expect(team.id, testTeam.id);
expect(team.name, testTeam.name);
expect(team.createdAt, testTeam.createdAt);
}
});
// Verifies that adding the same team twice does not create duplicates and returns false.
test('Adding the same team twice does not create duplicates', () async {
await database.teamDao.addTeam(team: testTeam1);
final addedAgain = await database.teamDao.addTeam(team: testTeam1);
expect(addedAgain, false);
final teamCount = await database.teamDao.getTeamCount();
expect(teamCount, 1);
});
// Verifies that teamExists returns correct boolean based on team presence.
test('Team existence check works correctly', () async {
var teamExists = await database.teamDao.teamExists(teamId: testTeam1.id);
expect(teamExists, false);
await database.teamDao.addTeam(team: testTeam1);
teamExists = await database.teamDao.teamExists(teamId: testTeam1.id);
expect(teamExists, true);
});
// Verifies that deleteTeam removes the team and returns true.
test('Deleting a team works correctly', () async {
await database.teamDao.addTeam(team: testTeam1);
final teamDeleted = await database.teamDao.deleteTeam(
teamId: testTeam1.id,
);
expect(teamDeleted, true);
final teamExists = await database.teamDao.teamExists(
teamId: testTeam1.id,
);
expect(teamExists, false);
});
// Verifies that deleteTeam returns false for a non-existent team ID.
test('Deleting a non-existent team returns false', () async {
final teamDeleted = await database.teamDao.deleteTeam(
teamId: 'non-existent-id',
);
expect(teamDeleted, false);
});
// Verifies that getTeamCount returns correct count through add/delete operations.
test('Getting the team count works correctly', () async {
var teamCount = await database.teamDao.getTeamCount();
expect(teamCount, 0);
await database.teamDao.addTeam(team: testTeam1);
teamCount = await database.teamDao.getTeamCount();
expect(teamCount, 1);
await database.teamDao.addTeam(team: testTeam2);
teamCount = await database.teamDao.getTeamCount();
expect(teamCount, 2);
await database.teamDao.deleteTeam(teamId: testTeam1.id);
teamCount = await database.teamDao.getTeamCount();
expect(teamCount, 1);
await database.teamDao.deleteTeam(teamId: testTeam2.id);
teamCount = await database.teamDao.getTeamCount();
expect(teamCount, 0);
});
// Verifies that updateTeamName correctly updates only the name field.
test('Updating team name works correctly', () async {
await database.teamDao.addTeam(team: testTeam1);
var fetchedTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id,
);
expect(fetchedTeam.name, testTeam1.name);
const newName = 'Updated Team Name';
await database.teamDao.updateTeamName(
teamId: testTeam1.id,
newName: newName,
);
fetchedTeam = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(fetchedTeam.name, newName);
});
// Verifies that deleteAllTeams removes all teams from the database.
test('Deleting all teams works correctly', () async {
await database.teamDao.addTeamsAsList(
teams: [testTeam1, testTeam2, testTeam3],
);
var teamCount = await database.teamDao.getTeamCount();
expect(teamCount, 3);
final deleted = await database.teamDao.deleteAllTeams();
expect(deleted, true);
teamCount = await database.teamDao.getTeamCount();
expect(teamCount, 0);
});
// Verifies that deleteAllTeams returns false when no teams exist.
test('Deleting all teams when empty returns false', () async {
final deleted = await database.teamDao.deleteAllTeams();
expect(deleted, false);
});
// Verifies that addTeamsAsList returns false when given an empty list.
test('Adding teams as list with empty list returns false', () async {
final added = await database.teamDao.addTeamsAsList(teams: []);
expect(added, false);
});
// Verifies that addTeamsAsList with duplicate IDs ignores duplicates and keeps the first.
test('Adding teams with duplicate IDs ignores duplicates', () async {
final duplicateTeam = Team(
id: testTeam1.id,
name: 'Duplicate Team',
members: [testPlayer4],
);
await database.teamDao.addTeamsAsList(
teams: [testTeam1, duplicateTeam, testTeam2],
);
final teamCount = await database.teamDao.getTeamCount();
expect(teamCount, 2);
// The first one should be kept (insertOrIgnore)
final fetchedTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id,
);
expect(fetchedTeam.name, testTeam1.name);
});
// Verifies that getAllTeams returns empty list when no teams exist.
test('Getting all teams when empty returns empty list', () async {
final allTeams = await database.teamDao.getAllTeams();
expect(allTeams.isEmpty, true);
});
// Verifies that getTeamById throws exception for non-existent team.
test('Getting non-existent team throws exception', () async {
expect(
() => database.teamDao.getTeamById(teamId: 'non-existent-id'),
throwsA(isA<StateError>()),
);
});
// Verifies that updating team name preserves other fields.
test('Updating team name preserves other team fields', () async {
await database.teamDao.addTeam(team: testTeam1);
final originalTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id,
);
final originalCreatedAt = originalTeam.createdAt;
const newName = 'Brand New Team Name';
await database.teamDao.updateTeamName(
teamId: testTeam1.id,
newName: newName,
);
final updatedTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id,
);
expect(updatedTeam.name, newName);
expect(updatedTeam.id, testTeam1.id);
expect(updatedTeam.createdAt, originalCreatedAt);
});
// Verifies that team name can be updated to an empty string.
test('Updating team name to empty string works', () async {
await database.teamDao.addTeam(team: testTeam1);
await database.teamDao.updateTeamName(
teamId: testTeam1.id,
newName: '',
);
final updatedTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id,
);
expect(updatedTeam.name, '');
});
// Verifies that team name can be updated to a very long string.
test('Updating team name to long string works', () async {
await database.teamDao.addTeam(team: testTeam1);
final longName = 'A' * 500; // 500 character name
await database.teamDao.updateTeamName(
teamId: testTeam1.id,
newName: longName,
);
final updatedTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id,
);
expect(updatedTeam.name, longName);
expect(updatedTeam.name.length, 500);
});
// Verifies that updating non-existent team name doesn't throw error.
test('Updating non-existent team name completes without error', () async {
expect(
() => database.teamDao.updateTeamName(
teamId: 'non-existent-id',
newName: 'New Name',
),
returnsNormally,
);
});
// Verifies that deleteTeam only affects the specified team.
test('Deleting one team does not affect other teams', () async {
await database.teamDao.addTeamsAsList(
teams: [testTeam1, testTeam2, testTeam3],
);
await database.teamDao.deleteTeam(teamId: testTeam2.id);
final allTeams = await database.teamDao.getAllTeams();
expect(allTeams.length, 2);
expect(allTeams.any((t) => t.id == testTeam1.id), true);
expect(allTeams.any((t) => t.id == testTeam2.id), false);
expect(allTeams.any((t) => t.id == testTeam3.id), true);
});
// Verifies that teams with overlapping members are independent.
test('Teams with overlapping members are independent', () async {
// Create two matches since player_match has primary key {playerId, matchId}
final match1 = Match(name: 'Match 1', game: testGame1);
final match2 = Match(name: 'Match 2', game: testGame2);
await database.matchDao.addMatch(match: match1);
await database.matchDao.addMatch(match: match2);
// Add teams to database
await database.teamDao.addTeamsAsList(
teams: [testTeam1, testTeam3],
);
// Associate players with teams through match1
// testTeam1: player1, player2
await database.playerMatchDao.addPlayerToMatch(
playerId: testPlayer1.id,
matchId: match1.id,
teamId: testTeam1.id,
score: 0,
);
await database.playerMatchDao.addPlayerToMatch(
playerId: testPlayer2.id,
matchId: match1.id,
teamId: testTeam1.id,
score: 0,
);
// Associate players with teams through match2
// testTeam3: player1, player3 (overlapping player1)
await database.playerMatchDao.addPlayerToMatch(
playerId: testPlayer1.id,
matchId: match2.id,
teamId: testTeam3.id,
score: 0,
);
await database.playerMatchDao.addPlayerToMatch(
playerId: testPlayer3.id,
matchId: match2.id,
teamId: testTeam3.id,
score: 0,
);
final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id);
final team3 = await database.teamDao.getTeamById(teamId: testTeam3.id);
expect(team1.members.length, 2);
expect(team3.members.length, 2);
expect(team1.members.any((p) => p.id == testPlayer1.id), true);
expect(team3.members.any((p) => p.id == testPlayer1.id), true);
});
// Verifies that adding teams sequentially works correctly.
test('Adding teams sequentially maintains correct count', () async {
var count = await database.teamDao.getTeamCount();
expect(count, 0);
await database.teamDao.addTeam(team: testTeam1);
count = await database.teamDao.getTeamCount();
expect(count, 1);
await database.teamDao.addTeam(team: testTeam2);
count = await database.teamDao.getTeamCount();
expect(count, 2);
await database.teamDao.addTeam(team: testTeam3);
count = await database.teamDao.getTeamCount();
expect(count, 3);
});
// Verifies that getAllTeams returns all teams with correct data.
test('Getting all teams returns all teams with correct data', () async {
await database.teamDao.addTeamsAsList(
teams: [testTeam1, testTeam2, testTeam3],
);
final allTeams = await database.teamDao.getAllTeams();
expect(allTeams.length, 3);
expect(
allTeams.map((t) => t.id).toSet(),
{testTeam1.id, testTeam2.id, testTeam3.id},
);
});
// Verifies that teamExists returns false for deleted teams.
test('Team existence returns false after deletion', () async {
await database.teamDao.addTeam(team: testTeam1);
expect(await database.teamDao.teamExists(teamId: testTeam1.id), true);
await database.teamDao.deleteTeam(teamId: testTeam1.id);
expect(await database.teamDao.teamExists(teamId: testTeam1.id), false);
});
// Verifies that adding multiple teams in batch then deleting returns correct count.
test('Batch add then partial delete maintains correct count', () async {
await database.teamDao.addTeamsAsList(
teams: [testTeam1, testTeam2, testTeam3],
);
expect(await database.teamDao.getTeamCount(), 3);
await database.teamDao.deleteTeam(teamId: testTeam1.id);
expect(await database.teamDao.getTeamCount(), 2);
await database.teamDao.deleteTeam(teamId: testTeam3.id);
expect(await database.teamDao.getTeamCount(), 1);
});
// Verifies that deleteAllTeams with single team works.
test('Deleting all teams with single team returns true', () async {
await database.teamDao.addTeam(team: testTeam1);
expect(await database.teamDao.getTeamCount(), 1);
final deleted = await database.teamDao.deleteAllTeams();
expect(deleted, true);
expect(await database.teamDao.getTeamCount(), 0);
});
// Verifies that addTeam after deleteAllTeams works correctly.
test('Adding team after deleteAllTeams works correctly', () async {
await database.teamDao.addTeamsAsList(
teams: [testTeam1, testTeam2],
);
expect(await database.teamDao.getTeamCount(), 2);
await database.teamDao.deleteAllTeams();
expect(await database.teamDao.getTeamCount(), 0);
final added = await database.teamDao.addTeam(team: testTeam3);
expect(added, true);
expect(await database.teamDao.getTeamCount(), 1);
final fetchedTeam = await database.teamDao.getTeamById(
teamId: testTeam3.id,
);
expect(fetchedTeam.name, testTeam3.name);
});
// Verifies that addTeamsAsList with partial duplicates ignores duplicates.
test('Adding teams with some duplicates ignores only duplicates', () async {
await database.teamDao.addTeam(team: testTeam1);
final duplicateTeam1 = Team(
id: testTeam1.id,
name: 'Different Name',
members: [testPlayer3],
);
await database.teamDao.addTeamsAsList(
teams: [duplicateTeam1, testTeam2, testTeam3],
);
final allTeams = await database.teamDao.getAllTeams();
expect(allTeams.length, 3);
// Verify testTeam1 retained original name (was inserted first)
final team1 = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team1.name, testTeam1.name);
});
// Verifies that team IDs are preserved correctly.
test('Team IDs are preserved through add and retrieve', () async {
await database.teamDao.addTeam(team: testTeam1);
final fetchedTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id,
);
expect(fetchedTeam.id, testTeam1.id);
});
// Verifies that createdAt timestamps are preserved.
test('Team createdAt timestamps are preserved', () async {
await database.teamDao.addTeam(team: testTeam1);
final fetchedTeam = await database.teamDao.getTeamById(
teamId: testTeam1.id,
);
expect(fetchedTeam.createdAt, testTeam1.createdAt);
});
});
}