Skip to content

Commit 007ea48

Browse files
authored
Merge pull request #27 from LEO0331/lc/dev/newfeatures
overall testing above 90%
2 parents 636ef0d + 396665a commit 007ea48

File tree

2 files changed

+195
-1
lines changed

2 files changed

+195
-1
lines changed

test/presentation/browse/browse_pages_additional_test.dart

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class _InstrumentedRepository extends InMemorySurplusRepository {
2121
bool reservationStreamError = false;
2222
bool throwOnReserve = false;
2323
bool throwOnAbuseSignal = false;
24+
bool watchListingCalledAfterRetry = false;
25+
bool watchVenuesCalledAfterRetry = false;
2426
String? lastAbuseReason;
2527
int reconcileCalls = 0;
2628

@@ -30,6 +32,7 @@ class _InstrumentedRepository extends InMemorySurplusRepository {
3032

3133
@override
3234
Stream<Listing?> watchListing(String listingId) {
35+
watchListingCalledAfterRetry = true;
3336
if (listingStreamErrorIds.contains(listingId)) {
3437
return Stream.error(StateError('listing stream failed'));
3538
}
@@ -42,6 +45,7 @@ class _InstrumentedRepository extends InMemorySurplusRepository {
4245

4346
@override
4447
Stream<List<Venue>> watchVenues() {
48+
watchVenuesCalledAfterRetry = true;
4549
if (venuesStreamError) {
4650
return Stream.error(StateError('venue stream failed'));
4751
}
@@ -295,6 +299,43 @@ void main() {
295299
},
296300
);
297301

302+
testWidgets('listings page supports near-hubs and available-now filters', (
303+
tester,
304+
) async {
305+
final repo = _InstrumentedRepository();
306+
final now = DateTime.now();
307+
await repo.createListing(
308+
_input(
309+
now,
310+
venueId: 'taipei-nangang-exhibition-center-hall-1',
311+
itemType: 'Near Hub Item',
312+
expiresIn: const Duration(hours: 2),
313+
),
314+
);
315+
await repo.createListing(
316+
_input(
317+
now.subtract(const Duration(hours: 3)),
318+
venueId: 'taipei-nangang-exhibition-center-hall-2',
319+
itemType: 'Expired Window Item',
320+
expiresIn: const Duration(hours: 4),
321+
),
322+
);
323+
324+
await _pumpHome(tester, repo);
325+
326+
await tester.tap(find.text('Near hubs'));
327+
await tester.pumpAndSettle();
328+
expect(find.text('Clear filter'), findsOneWidget);
329+
330+
await tester.tap(find.text('Available now'));
331+
await tester.pumpAndSettle();
332+
expect(find.text('Clear filter'), findsOneWidget);
333+
334+
await tester.tap(find.text('Clear filter'));
335+
await tester.pumpAndSettle();
336+
expect(find.text('Clear filter'), findsNothing);
337+
});
338+
298339
testWidgets('listings page shows load error when venue stream fails', (
299340
tester,
300341
) async {
@@ -378,6 +419,9 @@ void main() {
378419
..listingStreamErrorIds.add('bad-listing');
379420
await _pumpDetail(tester, repo, 'bad-listing');
380421
expect(find.text('Unable to load'), findsOneWidget);
422+
await tester.tap(find.widgetWithText(FilledButton, 'Retry'));
423+
await tester.pumpAndSettle();
424+
expect(repo.watchListingCalledAfterRetry, isTrue);
381425
});
382426

383427
testWidgets('listing detail shows load error when venues stream fails', (
@@ -393,8 +437,56 @@ void main() {
393437
);
394438
await _pumpDetail(tester, repo, created.listingId);
395439
expect(find.text('Unable to load'), findsOneWidget);
440+
await tester.tap(find.widgetWithText(FilledButton, 'Retry'));
441+
await tester.pumpAndSettle();
442+
expect(repo.watchVenuesCalledAfterRetry, isTrue);
396443
});
397444

445+
testWidgets(
446+
'listing detail reserve success falls back to navigator when go_router is absent',
447+
(tester) async {
448+
final repo = _InstrumentedRepository();
449+
final created = await repo.createListing(
450+
_input(
451+
DateTime.now(),
452+
venueId: 'taipei-nangang-exhibition-center-hall-1',
453+
itemType: 'Fallback flow',
454+
),
455+
);
456+
await _pumpDetail(tester, repo, created.listingId);
457+
458+
await tester.tap(find.text('Reserve 1 item'));
459+
await tester.pumpAndSettle();
460+
await tester.tap(find.byType(CheckboxListTile));
461+
await tester.pumpAndSettle();
462+
await tester.tap(find.widgetWithText(FilledButton, 'Reserve'));
463+
await tester.pumpAndSettle();
464+
465+
expect(find.text('Reservation confirmed'), findsOneWidget);
466+
},
467+
);
468+
469+
testWidgets(
470+
'listing detail renders badge chips and filters unknown badge id',
471+
(tester) async {
472+
final now = DateTime.now();
473+
final repo = _InstrumentedRepository()
474+
..forcedListings['badge-id'] = _forcedListing(
475+
now,
476+
id: 'badge-id',
477+
status: ListingStatus.active,
478+
quantityRemaining: 1,
479+
expiresAt: now.add(const Duration(hours: 1)),
480+
displayNameOptional: 'Badge Enterprise',
481+
enterpriseBadges: const <String>['verified', 'unknown_badge'],
482+
);
483+
484+
await _pumpDetail(tester, repo, 'badge-id');
485+
expect(find.text('Verified enterprise'), findsOneWidget);
486+
expect(find.text('unknown_badge'), findsNothing);
487+
},
488+
);
489+
398490
testWidgets(
399491
'listing detail renders reserved, expired and completed statuses',
400492
(tester) async {

test/presentation/browse/my_reservations_page_test.dart

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import 'dart:async';
2+
13
import 'package:boxmatch/app/app_scope.dart';
24
import 'package:boxmatch/features/surplus/data/in_memory_surplus_repository.dart';
35
import 'package:boxmatch/features/surplus/domain/listing_input.dart';
46
import 'package:boxmatch/features/surplus/domain/listing_visibility.dart';
7+
import 'package:boxmatch/features/surplus/domain/reservation.dart';
8+
import 'package:boxmatch/features/surplus/domain/surplus_exceptions.dart';
59
import 'package:boxmatch/features/surplus/presentation/browse/my_reservations_page.dart';
610
import 'package:flutter/material.dart';
711
import 'package:flutter_test/flutter_test.dart';
@@ -44,6 +48,36 @@ Future<void> _pumpPage(
4448
await tester.pumpAndSettle();
4549
}
4650

51+
class _ThrowingListRepository extends InMemorySurplusRepository {
52+
@override
53+
Future<List<Reservation>> listRecipientReservations({
54+
required String claimerUid,
55+
}) async {
56+
throw const ValidationException('List failed for test.');
57+
}
58+
}
59+
60+
class _PendingListRepository extends InMemorySurplusRepository {
61+
final Completer<List<Reservation>> completer = Completer<List<Reservation>>();
62+
63+
@override
64+
Future<List<Reservation>> listRecipientReservations({
65+
required String claimerUid,
66+
}) async {
67+
return completer.future;
68+
}
69+
}
70+
71+
class _ThrowingCancelRepository extends InMemorySurplusRepository {
72+
@override
73+
Future<void> cancelReservation({
74+
required String reservationId,
75+
required String claimerUid,
76+
}) async {
77+
throw const ValidationException('Cancel failed for test.');
78+
}
79+
}
80+
4781
void main() {
4882
testWidgets('shows privacy/faq without client-side inferred badge', (
4983
tester,
@@ -104,6 +138,74 @@ void main() {
104138
await _pumpPage(tester, repo: repo);
105139

106140
expect(find.text('No reservations yet.'), findsOneWidget);
107-
expect(find.widgetWithText(FilledButton, 'Browse listings'), findsOneWidget);
141+
expect(
142+
find.widgetWithText(FilledButton, 'Browse listings'),
143+
findsOneWidget,
144+
);
145+
});
146+
147+
testWidgets('shows loading skeleton while waiting for reservations', (
148+
tester,
149+
) async {
150+
final repo = _PendingListRepository();
151+
final deps = await buildTestDependencies(repository: repo);
152+
tester.view.physicalSize = const Size(1280, 2000);
153+
tester.view.devicePixelRatio = 1.0;
154+
addTearDown(tester.view.resetPhysicalSize);
155+
addTearDown(tester.view.resetDevicePixelRatio);
156+
157+
await tester.pumpWidget(
158+
AppScope(
159+
dependencies: deps,
160+
child: const MaterialApp(home: MyReservationsPage()),
161+
),
162+
);
163+
await tester.pump(const Duration(milliseconds: 200));
164+
165+
expect(find.byType(ConstrainedBox), findsWidgets);
166+
expect(find.byType(Card), findsWidgets);
167+
repo.completer.complete(const <Reservation>[]);
168+
});
169+
170+
testWidgets('shows error view and warmup hint after retry fails', (
171+
tester,
172+
) async {
173+
final repo = _ThrowingListRepository();
174+
final deps = await buildTestDependencies(repository: repo);
175+
176+
await tester.pumpWidget(
177+
AppScope(
178+
dependencies: deps,
179+
child: const MaterialApp(home: MyReservationsPage()),
180+
),
181+
);
182+
await tester.pump(const Duration(milliseconds: 900));
183+
await tester.pumpAndSettle();
184+
185+
expect(find.text('Unable to load'), findsOneWidget);
186+
expect(
187+
find.textContaining('Service may still be warming up'),
188+
findsOneWidget,
189+
);
190+
});
191+
192+
testWidgets('cancel reservation error shows snackbar', (tester) async {
193+
final repo = _ThrowingCancelRepository();
194+
final now = DateTime.now();
195+
final listing = await repo.createListing(
196+
_input(now, itemType: 'Lunchbox', displayName: 'Acme Charity'),
197+
);
198+
await repo.reserveListing(
199+
listingId: listing.listingId,
200+
claimerUid: 'test-user',
201+
qty: 1,
202+
disclaimerAccepted: true,
203+
);
204+
205+
await _pumpPage(tester, repo: repo);
206+
await tester.tap(find.widgetWithText(OutlinedButton, 'Cancel reservation'));
207+
await tester.pumpAndSettle();
208+
209+
expect(find.text('Cancel failed for test.'), findsOneWidget);
108210
});
109211
}

0 commit comments

Comments
 (0)