Skip to content

Commit 16102e2

Browse files
committed
test: cover drink-detail navigation paths for codecov/patch
Add widget tests exercising the navigateToRoute-based navigation introduced in this PR: brewery, drinks, favourites and drink-detail screens now have tests that trigger navigation to a drink detail and verify the destination renders. This raises patch coverage above the codecov/patch 70% threshold. https://claude.ai/code/session_01FqdVsDvtHXyWSszZA8tvno
1 parent 090a8d6 commit 16102e2

5 files changed

Lines changed: 242 additions & 15 deletions

File tree

test/brewery_screen_test.dart

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:cambridge_beer_festival/models/models.dart';
55
import 'package:cambridge_beer_festival/providers/providers.dart';
66
import 'package:cambridge_beer_festival/services/services.dart';
77
import 'package:cambridge_beer_festival/widgets/widgets.dart';
8+
import 'package:go_router/go_router.dart';
89
import 'package:provider/provider.dart';
910
import 'package:mockito/mockito.dart';
1011
import 'package:shared_preferences/shared_preferences.dart';
@@ -134,15 +135,33 @@ void main() {
134135
.thenAnswer((_) async => [drink1]);
135136
await provider.loadDrinks();
136137

137-
await tester.pumpWidget(createTestWidget('brewery1'));
138+
final router = GoRouter(
139+
initialLocation: '/brewery',
140+
routes: [
141+
GoRoute(
142+
path: '/brewery',
143+
builder: (context, state) => ChangeNotifierProvider<BeerProvider>.value(
144+
value: provider,
145+
child: const BreweryScreen(festivalId: 'cbf2025', breweryId: 'brewery1'),
146+
),
147+
),
148+
GoRoute(
149+
path: '/cbf2025/drink/:category/:drinkId',
150+
builder: (context, state) => const Scaffold(body: Text('Drink Detail')),
151+
),
152+
],
153+
);
154+
155+
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
138156
await tester.pumpAndSettle();
139157

140-
// Find the drink card - this verifies the card is rendered and tappable
141158
expect(find.text('Test Beer 1'), findsOneWidget);
142-
143-
// NOTE: Navigation to DrinkDetailScreen uses go_router's context.push()
144-
// which requires GoRouter in the widget tree. This is tested in E2E tests
145-
// (test-e2e/routing.spec.ts) instead of unit tests.
159+
160+
final card = tester.widget<DrinkCard>(find.byKey(const ValueKey('drink1')));
161+
card.onTap!();
162+
await tester.pumpAndSettle();
163+
164+
expect(find.text('Drink Detail'), findsOneWidget);
146165
});
147166

148167
testWidgets('toggles favorite when favorite button is tapped',

test/drink_detail_screen_test.dart

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:cambridge_beer_festival/screens/screens.dart';
44
import 'package:cambridge_beer_festival/models/models.dart';
55
import 'package:cambridge_beer_festival/providers/providers.dart';
66
import 'package:cambridge_beer_festival/services/services.dart';
7+
import 'package:go_router/go_router.dart';
78
import 'package:provider/provider.dart';
89
import 'package:mockito/mockito.dart';
910
import 'package:shared_preferences/shared_preferences.dart';
@@ -86,6 +87,30 @@ void main() {
8687
);
8788
}
8889

90+
Widget createTestWidgetWithRouter(String drinkId) {
91+
final router = GoRouter(
92+
initialLocation: '/cbf2025/drink/beer/$drinkId',
93+
routes: [
94+
GoRoute(
95+
path: '/cbf2025/drink/:category/:drinkId',
96+
builder: (context, state) => ChangeNotifierProvider<BeerProvider>.value(
97+
value: provider,
98+
child: DrinkDetailScreen(festivalId: 'cbf2025', drinkId: state.pathParameters['drinkId']!),
99+
),
100+
),
101+
GoRoute(
102+
path: '/cbf2025/brewery/:breweryId',
103+
builder: (context, state) => const Scaffold(body: Text('Brewery Screen')),
104+
),
105+
GoRoute(
106+
path: '/cbf2025/style/:style',
107+
builder: (context, state) => const Scaffold(body: Text('Style Screen')),
108+
),
109+
],
110+
);
111+
return MaterialApp.router(routerConfig: router);
112+
}
113+
89114
testWidgets('displays drink not found when drink does not exist',
90115
(WidgetTester tester) async {
91116
await tester.pumpWidget(createTestWidget('nonexistent'));
@@ -296,23 +321,39 @@ void main() {
296321
.thenAnswer((_) async => [drink]);
297322
await provider.loadDrinks();
298323

299-
await tester.pumpWidget(createTestWidget('drink1'));
324+
await tester.pumpWidget(createTestWidgetWithRouter('drink1'));
300325
await tester.pumpAndSettle();
301326

302-
// Find brewery card and ensure it's visible
303327
final breweryCard = find.ancestor(
304328
of: find.text('Test Brewery'),
305329
matching: find.byType(Card),
306330
);
307331
await tester.ensureVisible(breweryCard.last);
308332
await tester.pumpAndSettle();
309-
310-
// Verify the brewery card is present and tappable
311-
expect(breweryCard, findsWidgets);
312-
313-
// NOTE: Navigation to BreweryScreen uses go_router's context.push()
314-
// which requires GoRouter in the widget tree. This is tested in E2E tests
315-
// (test-e2e/routing.spec.ts) instead of unit tests.
333+
334+
await tester.tap(breweryCard.last);
335+
await tester.pumpAndSettle();
336+
337+
expect(find.text('Brewery Screen'), findsOneWidget);
338+
});
339+
340+
testWidgets('navigates to style screen when style chip is tapped',
341+
(WidgetTester tester) async {
342+
when(mockDrinkRepository.getDrinks(any))
343+
.thenAnswer((_) async => [drink]);
344+
await provider.loadDrinks();
345+
346+
await tester.pumpWidget(createTestWidgetWithRouter('drink1'));
347+
await tester.pumpAndSettle();
348+
349+
final styleChip = find.text('IPA');
350+
await tester.ensureVisible(styleChip);
351+
await tester.pumpAndSettle();
352+
353+
await tester.tap(styleChip);
354+
await tester.pumpAndSettle();
355+
356+
expect(find.text('Style Screen'), findsOneWidget);
316357
});
317358

318359
testWidgets('does not display description section when notes are null',

test/drinks_screen_style_filter_test.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:cambridge_beer_festival/screens/screens.dart';
55
import 'package:cambridge_beer_festival/models/models.dart';
66
import 'package:cambridge_beer_festival/providers/providers.dart';
77
import 'package:cambridge_beer_festival/services/services.dart';
8+
import 'package:go_router/go_router.dart';
89
import 'package:provider/provider.dart';
910
import 'package:mockito/mockito.dart';
1011
import 'package:shared_preferences/shared_preferences.dart';
@@ -120,6 +121,41 @@ void main() {
120121
);
121122
}
122123

124+
Widget createTestWidgetWithRouter() {
125+
final router = GoRouter(
126+
initialLocation: '/cbf2025/drinks',
127+
routes: [
128+
GoRoute(
129+
path: '/cbf2025/drinks',
130+
builder: (context, state) => ChangeNotifierProvider<BeerProvider>.value(
131+
value: provider,
132+
child: const DrinksScreen(festivalId: 'cbf2025'),
133+
),
134+
),
135+
GoRoute(
136+
path: '/cbf2025/drink/:category/:drinkId',
137+
builder: (context, state) => const Scaffold(body: Text('Drink Detail')),
138+
),
139+
],
140+
);
141+
return MaterialApp.router(routerConfig: router);
142+
}
143+
144+
testWidgets('navigates to drink detail when drink card is tapped',
145+
(WidgetTester tester) async {
146+
await tester.pumpWidget(createTestWidgetWithRouter());
147+
await tester.pumpAndSettle();
148+
149+
expect(find.text('Alpha IPA'), findsOneWidget);
150+
151+
final drinkCard = find.byKey(const ValueKey('drink1'));
152+
await tester.ensureVisible(drinkCard);
153+
await tester.tap(drinkCard);
154+
await tester.pumpAndSettle();
155+
156+
expect(find.text('Drink Detail'), findsOneWidget);
157+
});
158+
123159
testWidgets('style filter button shows when styles are available',
124160
(WidgetTester tester) async {
125161
await tester.pumpWidget(createTestWidget());

test/main_test.dart

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:cambridge_beer_festival/main.dart';
55
import 'package:cambridge_beer_festival/providers/beer_provider.dart';
66
import 'package:cambridge_beer_festival/services/services.dart';
77
import 'package:cambridge_beer_festival/models/models.dart';
8+
import 'package:go_router/go_router.dart';
89
import 'package:provider/provider.dart';
910
import 'package:mockito/mockito.dart';
1011
import 'package:shared_preferences/shared_preferences.dart';
@@ -302,4 +303,98 @@ void main() {
302303
verifyNever(mockDrinkRepository.getDrinks(any));
303304
});
304305
});
306+
307+
group('FavoritesScreen', () {
308+
late MockDrinkRepository mockDrinkRepository;
309+
late MockFestivalRepository mockFestivalRepository;
310+
late MockAnalyticsService mockAnalyticsService;
311+
late BeerProvider provider;
312+
313+
final favoriteDrink = Drink(
314+
product: const Product(
315+
id: 'drink1',
316+
name: 'Favourite Ale',
317+
abv: 4.5,
318+
category: 'beer',
319+
dispense: 'cask',
320+
),
321+
producer: const Producer(
322+
id: 'brewery1',
323+
name: 'Test Brewery',
324+
location: 'Cambridge',
325+
products: [],
326+
),
327+
festivalId: 'cbf2025',
328+
isFavorite: true,
329+
);
330+
331+
setUp(() async {
332+
SharedPreferences.setMockInitialValues({});
333+
mockDrinkRepository = MockDrinkRepository();
334+
mockFestivalRepository = MockFestivalRepository();
335+
mockAnalyticsService = MockAnalyticsService();
336+
337+
when(mockFestivalRepository.getFestivals()).thenAnswer(
338+
(_) async => FestivalsResponse(
339+
festivals: [
340+
const Festival(
341+
id: 'cbf2025',
342+
name: 'Cambridge Beer Festival 2025',
343+
dataBaseUrl: 'https://example.com',
344+
),
345+
],
346+
defaultFestivalId: 'cbf2025',
347+
version: '1.0',
348+
baseUrl: 'https://example.com',
349+
),
350+
);
351+
when(mockFestivalRepository.getSelectedFestivalId()).thenAnswer((_) async => null);
352+
when(mockDrinkRepository.getDrinks(any)).thenAnswer((_) async => [favoriteDrink]);
353+
when(mockDrinkRepository.getFavorites(any)).thenAnswer((_) async => ['drink1']);
354+
355+
provider = BeerProvider(
356+
drinkRepository: mockDrinkRepository,
357+
festivalRepository: mockFestivalRepository,
358+
analyticsService: mockAnalyticsService,
359+
);
360+
await provider.initialize();
361+
await provider.loadDrinks();
362+
});
363+
364+
tearDown(() {
365+
provider.dispose();
366+
});
367+
368+
testWidgets('navigates to drink detail when favorite drink card is tapped',
369+
(WidgetTester tester) async {
370+
final router = GoRouter(
371+
initialLocation: '/favorites',
372+
routes: [
373+
GoRoute(
374+
path: '/favorites',
375+
builder: (context, state) => ChangeNotifierProvider<BeerProvider>.value(
376+
value: provider,
377+
child: const FavoritesScreen(festivalId: 'cbf2025'),
378+
),
379+
),
380+
GoRoute(
381+
path: '/cbf2025/drink/:category/:drinkId',
382+
builder: (context, state) => const Scaffold(body: Text('Drink Detail')),
383+
),
384+
],
385+
);
386+
387+
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
388+
await tester.pumpAndSettle();
389+
390+
expect(find.text('Favourite Ale'), findsOneWidget);
391+
392+
final drinkCard = find.byKey(const ValueKey('drink1'));
393+
await tester.ensureVisible(drinkCard);
394+
await tester.tap(drinkCard);
395+
await tester.pumpAndSettle();
396+
397+
expect(find.text('Drink Detail'), findsOneWidget);
398+
});
399+
});
305400
}

test/utils/navigation_helpers_test.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:cambridge_beer_festival/utils/utils.dart';
22
import 'package:flutter/material.dart';
33
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:go_router/go_router.dart';
45

56
void main() {
67
group('Navigation Helpers', () {
@@ -324,6 +325,41 @@ void main() {
324325
});
325326
});
326327

328+
group('navigateToRoute', () {
329+
testWidgets('navigates to the specified path', (tester) async {
330+
bool detailVisited = false;
331+
final router = GoRouter(
332+
initialLocation: '/',
333+
routes: [
334+
GoRoute(
335+
path: '/',
336+
builder: (context, state) => Scaffold(
337+
body: Builder(
338+
builder: (context) => ElevatedButton(
339+
onPressed: () => navigateToRoute(context, '/detail'),
340+
child: const Text('Navigate'),
341+
),
342+
),
343+
),
344+
),
345+
GoRoute(
346+
path: '/detail',
347+
builder: (context, state) {
348+
detailVisited = true;
349+
return const Scaffold(body: Text('Detail'));
350+
},
351+
),
352+
],
353+
);
354+
355+
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
356+
await tester.tap(find.text('Navigate'));
357+
await tester.pumpAndSettle();
358+
359+
expect(detailVisited, isTrue);
360+
});
361+
});
362+
327363
group('canPopNavigation', () {
328364
testWidgets('returns false when GoRouter is not available', (tester) async {
329365
// In test environment with MaterialApp but without GoRouter

0 commit comments

Comments
 (0)