Skip to content

Commit 5c8624a

Browse files
committed
test: add BeerProvider tests for My Festival methods
Add comprehensive tests for the 4 new BeerProvider methods introduced in Phase 3.3: - getFavoriteStatus() - returns status from repository or null - getTryCount() - returns count from repository or 0 - markAsTasted() - updates state, notifies listeners, logs analytics - deleteTry() - removes timestamps, updates state, reapplies filters Also fixes a critical bug in FavoriteItem.copyWith() where nullable parameters couldn't be explicitly set to null. Introduced Optional<T> wrapper class to handle this common Dart pattern issue. Changes: - test/beer_provider_test.dart: Added 13 tests for My Festival methods - lib/models/favorite_item.dart: Fixed copyWith with Optional wrapper - lib/services/storage_service.dart: Updated updateNotes to use Optional All 578 tests passing. Code analysis clean.
1 parent 6a55f26 commit 5c8624a

4 files changed

Lines changed: 429 additions & 3 deletions

File tree

lib/models/favorite_item.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,22 @@ class FavoriteItem {
5959
}
6060

6161
/// Creates a copy with updated fields.
62+
///
63+
/// To explicitly clear notes, pass an empty Optional: `notes: Optional.value(null)`.
64+
/// To keep existing notes, omit the parameter: `copyWith(status: 'tasted')`.
6265
FavoriteItem copyWith({
6366
String? id,
6467
String? status,
6568
List<DateTime>? tries,
66-
String? notes,
69+
Optional<String?>? notes,
6770
DateTime? createdAt,
6871
DateTime? updatedAt,
6972
}) {
7073
return FavoriteItem(
7174
id: id ?? this.id,
7275
status: status ?? this.status,
7376
tries: tries ?? this.tries,
74-
notes: notes ?? this.notes,
77+
notes: notes != null ? notes.value : this.notes,
7578
createdAt: createdAt ?? this.createdAt,
7679
updatedAt: updatedAt ?? this.updatedAt,
7780
);
@@ -87,3 +90,10 @@ class FavoriteItem {
8790
@override
8891
int get hashCode => id.hashCode;
8992
}
93+
94+
/// Wrapper class for explicitly passing null values in copyWith methods.
95+
class Optional<T> {
96+
const Optional.value(this.value);
97+
98+
final T value;
99+
}

lib/services/storage_service.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ class FavoritesService {
167167
if (existing == null) return;
168168

169169
favorites[drinkId] = existing.copyWith(
170-
notes: notes,
170+
notes: Optional.value(notes),
171171
updatedAt: DateTime.now(),
172172
);
173173

test/beer_provider_test.dart

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,307 @@ void main() {
11501150
});
11511151
});
11521152

1153+
group('My Festival methods', () {
1154+
test('getFavoriteStatus returns status from repository', () async {
1155+
provider = BeerProvider(
1156+
drinkRepository: mockDrinkRepository,
1157+
festivalRepository: mockFestivalRepository,
1158+
analyticsService: mockAnalyticsService,
1159+
);
1160+
await provider.initialize();
1161+
1162+
final sampleDrinks = createSampleDrinks();
1163+
when(mockDrinkRepository.getDrinks(any))
1164+
.thenAnswer((_) async => sampleDrinks);
1165+
await provider.loadDrinks();
1166+
1167+
when(mockDrinkRepository.getFavoriteStatus(any, any))
1168+
.thenAnswer((_) async => 'want_to_try');
1169+
1170+
final status = await provider.getFavoriteStatus(sampleDrinks[0]);
1171+
expect(status, 'want_to_try');
1172+
verify(mockDrinkRepository.getFavoriteStatus('cbf2025', 'drink-1')).called(1);
1173+
});
1174+
1175+
test('getFavoriteStatus returns null when no repository', () async {
1176+
provider = BeerProvider(
1177+
drinkRepository: mockDrinkRepository,
1178+
festivalRepository: mockFestivalRepository,
1179+
analyticsService: mockAnalyticsService,
1180+
);
1181+
1182+
final drink = createSampleDrinks()[0];
1183+
final status = await provider.getFavoriteStatus(drink);
1184+
expect(status, isNull);
1185+
});
1186+
1187+
test('getTryCount returns count from repository', () async {
1188+
provider = BeerProvider(
1189+
drinkRepository: mockDrinkRepository,
1190+
festivalRepository: mockFestivalRepository,
1191+
analyticsService: mockAnalyticsService,
1192+
);
1193+
await provider.initialize();
1194+
1195+
final sampleDrinks = createSampleDrinks();
1196+
when(mockDrinkRepository.getDrinks(any))
1197+
.thenAnswer((_) async => sampleDrinks);
1198+
await provider.loadDrinks();
1199+
1200+
when(mockDrinkRepository.getTryCount(any, any))
1201+
.thenAnswer((_) async => 3);
1202+
1203+
final count = await provider.getTryCount(sampleDrinks[0]);
1204+
expect(count, 3);
1205+
verify(mockDrinkRepository.getTryCount('cbf2025', 'drink-1')).called(1);
1206+
});
1207+
1208+
test('getTryCount returns 0 when no repository', () async {
1209+
provider = BeerProvider(
1210+
drinkRepository: mockDrinkRepository,
1211+
festivalRepository: mockFestivalRepository,
1212+
analyticsService: mockAnalyticsService,
1213+
);
1214+
1215+
final drink = createSampleDrinks()[0];
1216+
final count = await provider.getTryCount(drink);
1217+
expect(count, 0);
1218+
});
1219+
1220+
test('markAsTasted updates drink state and notifies listeners', () async {
1221+
provider = BeerProvider(
1222+
drinkRepository: mockDrinkRepository,
1223+
festivalRepository: mockFestivalRepository,
1224+
analyticsService: mockAnalyticsService,
1225+
);
1226+
await provider.initialize();
1227+
1228+
final sampleDrinks = createSampleDrinks();
1229+
when(mockDrinkRepository.getDrinks(any))
1230+
.thenAnswer((_) async => sampleDrinks);
1231+
await provider.loadDrinks();
1232+
1233+
when(mockDrinkRepository.markAsTasted(any, any))
1234+
.thenAnswer((_) async {});
1235+
when(mockDrinkRepository.getTryCount(any, any))
1236+
.thenAnswer((_) async => 1);
1237+
1238+
final drink = sampleDrinks[0];
1239+
expect(drink.isFavorite, isFalse);
1240+
1241+
var notified = false;
1242+
provider.addListener(() {
1243+
notified = true;
1244+
});
1245+
1246+
await provider.markAsTasted(drink);
1247+
1248+
expect(drink.isFavorite, isTrue);
1249+
expect(notified, isTrue);
1250+
verify(mockDrinkRepository.markAsTasted('cbf2025', 'drink-1')).called(1);
1251+
});
1252+
1253+
test('markAsTasted logs first tasting event', () async {
1254+
provider = BeerProvider(
1255+
drinkRepository: mockDrinkRepository,
1256+
festivalRepository: mockFestivalRepository,
1257+
analyticsService: mockAnalyticsService,
1258+
);
1259+
await provider.initialize();
1260+
1261+
final sampleDrinks = createSampleDrinks();
1262+
when(mockDrinkRepository.getDrinks(any))
1263+
.thenAnswer((_) async => sampleDrinks);
1264+
await provider.loadDrinks();
1265+
1266+
when(mockDrinkRepository.markAsTasted(any, any))
1267+
.thenAnswer((_) async {});
1268+
when(mockDrinkRepository.getTryCount(any, any))
1269+
.thenAnswer((_) async => 1);
1270+
1271+
await provider.markAsTasted(sampleDrinks[0]);
1272+
1273+
await Future.delayed(const Duration(milliseconds: 10));
1274+
1275+
verify(mockAnalyticsService.logFestivalLogMarkTasted('drink-1', 1)).called(1);
1276+
verifyNever(mockAnalyticsService.logFestivalLogMultipleTasting(any, any));
1277+
});
1278+
1279+
test('markAsTasted logs multiple tasting event when count > 1', () async {
1280+
provider = BeerProvider(
1281+
drinkRepository: mockDrinkRepository,
1282+
festivalRepository: mockFestivalRepository,
1283+
analyticsService: mockAnalyticsService,
1284+
);
1285+
await provider.initialize();
1286+
1287+
final sampleDrinks = createSampleDrinks();
1288+
when(mockDrinkRepository.getDrinks(any))
1289+
.thenAnswer((_) async => sampleDrinks);
1290+
await provider.loadDrinks();
1291+
1292+
when(mockDrinkRepository.markAsTasted(any, any))
1293+
.thenAnswer((_) async {});
1294+
when(mockDrinkRepository.getTryCount(any, any))
1295+
.thenAnswer((_) async => 3);
1296+
1297+
await provider.markAsTasted(sampleDrinks[0]);
1298+
1299+
await Future.delayed(const Duration(milliseconds: 10));
1300+
1301+
verify(mockAnalyticsService.logFestivalLogMultipleTasting('drink-1', 3)).called(1);
1302+
verifyNever(mockAnalyticsService.logFestivalLogMarkTasted(any, any));
1303+
});
1304+
1305+
test('markAsTasted does nothing when no repository', () async {
1306+
provider = BeerProvider(
1307+
drinkRepository: null,
1308+
festivalRepository: mockFestivalRepository,
1309+
analyticsService: mockAnalyticsService,
1310+
);
1311+
1312+
final drink = createSampleDrinks()[0];
1313+
final initialFavorite = drink.isFavorite;
1314+
1315+
await provider.markAsTasted(drink);
1316+
1317+
expect(drink.isFavorite, initialFavorite);
1318+
});
1319+
1320+
test('deleteTry removes timestamp and updates drink state', () async {
1321+
provider = BeerProvider(
1322+
drinkRepository: mockDrinkRepository,
1323+
festivalRepository: mockFestivalRepository,
1324+
analyticsService: mockAnalyticsService,
1325+
);
1326+
await provider.initialize();
1327+
1328+
final sampleDrinks = createSampleDrinks();
1329+
when(mockDrinkRepository.getDrinks(any))
1330+
.thenAnswer((_) async => sampleDrinks);
1331+
await provider.loadDrinks();
1332+
1333+
final timestamp = DateTime.now();
1334+
when(mockDrinkRepository.deleteTry(any, any, any))
1335+
.thenAnswer((_) async {});
1336+
when(mockDrinkRepository.getFavoriteStatus(any, any))
1337+
.thenAnswer((_) async => 'tasted');
1338+
1339+
final drink = sampleDrinks[0];
1340+
drink.isFavorite = true;
1341+
1342+
var notified = false;
1343+
provider.addListener(() {
1344+
notified = true;
1345+
});
1346+
1347+
await provider.deleteTry(drink, timestamp);
1348+
1349+
expect(drink.isFavorite, isTrue);
1350+
expect(notified, isTrue);
1351+
verify(mockDrinkRepository.deleteTry('cbf2025', 'drink-1', timestamp)).called(1);
1352+
});
1353+
1354+
test('deleteTry updates isFavorite to false when status becomes null', () async {
1355+
provider = BeerProvider(
1356+
drinkRepository: mockDrinkRepository,
1357+
festivalRepository: mockFestivalRepository,
1358+
analyticsService: mockAnalyticsService,
1359+
);
1360+
await provider.initialize();
1361+
1362+
final sampleDrinks = createSampleDrinks();
1363+
when(mockDrinkRepository.getDrinks(any))
1364+
.thenAnswer((_) async => sampleDrinks);
1365+
await provider.loadDrinks();
1366+
1367+
final timestamp = DateTime.now();
1368+
when(mockDrinkRepository.deleteTry(any, any, any))
1369+
.thenAnswer((_) async {});
1370+
when(mockDrinkRepository.getFavoriteStatus(any, any))
1371+
.thenAnswer((_) async => null);
1372+
1373+
final drink = sampleDrinks[0];
1374+
drink.isFavorite = true;
1375+
1376+
await provider.deleteTry(drink, timestamp);
1377+
1378+
expect(drink.isFavorite, isFalse);
1379+
});
1380+
1381+
test('deleteTry reapplies filters when showing favorites only', () async {
1382+
provider = BeerProvider(
1383+
drinkRepository: mockDrinkRepository,
1384+
festivalRepository: mockFestivalRepository,
1385+
analyticsService: mockAnalyticsService,
1386+
);
1387+
await provider.initialize();
1388+
1389+
final sampleDrinks = createSampleDrinks();
1390+
when(mockDrinkRepository.getDrinks(any))
1391+
.thenAnswer((_) async => sampleDrinks);
1392+
await provider.loadDrinks();
1393+
1394+
// Mark first drink as favorite
1395+
sampleDrinks[0].isFavorite = true;
1396+
provider.setShowFavoritesOnly(true);
1397+
1398+
final timestamp = DateTime.now();
1399+
when(mockDrinkRepository.deleteTry(any, any, any))
1400+
.thenAnswer((_) async {});
1401+
when(mockDrinkRepository.getFavoriteStatus(any, any))
1402+
.thenAnswer((_) async => null);
1403+
1404+
expect(provider.drinks.length, 1);
1405+
1406+
await provider.deleteTry(sampleDrinks[0], timestamp);
1407+
1408+
expect(provider.drinks.length, 0);
1409+
});
1410+
1411+
test('deleteTry logs analytics event', () async {
1412+
provider = BeerProvider(
1413+
drinkRepository: mockDrinkRepository,
1414+
festivalRepository: mockFestivalRepository,
1415+
analyticsService: mockAnalyticsService,
1416+
);
1417+
await provider.initialize();
1418+
1419+
final sampleDrinks = createSampleDrinks();
1420+
when(mockDrinkRepository.getDrinks(any))
1421+
.thenAnswer((_) async => sampleDrinks);
1422+
await provider.loadDrinks();
1423+
1424+
final timestamp = DateTime.now();
1425+
when(mockDrinkRepository.deleteTry(any, any, any))
1426+
.thenAnswer((_) async {});
1427+
when(mockDrinkRepository.getFavoriteStatus(any, any))
1428+
.thenAnswer((_) async => 'tasted');
1429+
1430+
await provider.deleteTry(sampleDrinks[0], timestamp);
1431+
1432+
await Future.delayed(const Duration(milliseconds: 10));
1433+
1434+
verify(mockAnalyticsService.logFestivalLogDeleteTimestamp('drink-1')).called(1);
1435+
});
1436+
1437+
test('deleteTry does nothing when no repository', () async {
1438+
provider = BeerProvider(
1439+
drinkRepository: null,
1440+
festivalRepository: mockFestivalRepository,
1441+
analyticsService: mockAnalyticsService,
1442+
);
1443+
1444+
final drink = createSampleDrinks()[0];
1445+
drink.isFavorite = true;
1446+
final timestamp = DateTime.now();
1447+
1448+
await provider.deleteTry(drink, timestamp);
1449+
1450+
expect(drink.isFavorite, isTrue);
1451+
});
1452+
});
1453+
11531454
group('automatic refresh', () {
11541455
test('isDrinksDataStale returns true when no data loaded', () {
11551456
provider = BeerProvider(

0 commit comments

Comments
 (0)