diff --git a/test/presentation/browse/browse_pages_additional_test.dart b/test/presentation/browse/browse_pages_additional_test.dart
index 8982035..6000243 100644
--- a/test/presentation/browse/browse_pages_additional_test.dart
+++ b/test/presentation/browse/browse_pages_additional_test.dart
@@ -21,6 +21,8 @@ class _InstrumentedRepository extends InMemorySurplusRepository {
bool reservationStreamError = false;
bool throwOnReserve = false;
bool throwOnAbuseSignal = false;
+ bool watchListingCalledAfterRetry = false;
+ bool watchVenuesCalledAfterRetry = false;
String? lastAbuseReason;
int reconcileCalls = 0;
@@ -30,6 +32,7 @@ class _InstrumentedRepository extends InMemorySurplusRepository {
@override
Stream
watchListing(String listingId) {
+ watchListingCalledAfterRetry = true;
if (listingStreamErrorIds.contains(listingId)) {
return Stream.error(StateError('listing stream failed'));
}
@@ -42,6 +45,7 @@ class _InstrumentedRepository extends InMemorySurplusRepository {
@override
Stream> watchVenues() {
+ watchVenuesCalledAfterRetry = true;
if (venuesStreamError) {
return Stream.error(StateError('venue stream failed'));
}
@@ -295,6 +299,43 @@ void main() {
},
);
+ testWidgets('listings page supports near-hubs and available-now filters', (
+ tester,
+ ) async {
+ final repo = _InstrumentedRepository();
+ final now = DateTime.now();
+ await repo.createListing(
+ _input(
+ now,
+ venueId: 'taipei-nangang-exhibition-center-hall-1',
+ itemType: 'Near Hub Item',
+ expiresIn: const Duration(hours: 2),
+ ),
+ );
+ await repo.createListing(
+ _input(
+ now.subtract(const Duration(hours: 3)),
+ venueId: 'taipei-nangang-exhibition-center-hall-2',
+ itemType: 'Expired Window Item',
+ expiresIn: const Duration(hours: 4),
+ ),
+ );
+
+ await _pumpHome(tester, repo);
+
+ await tester.tap(find.text('Near hubs'));
+ await tester.pumpAndSettle();
+ expect(find.text('Clear filter'), findsOneWidget);
+
+ await tester.tap(find.text('Available now'));
+ await tester.pumpAndSettle();
+ expect(find.text('Clear filter'), findsOneWidget);
+
+ await tester.tap(find.text('Clear filter'));
+ await tester.pumpAndSettle();
+ expect(find.text('Clear filter'), findsNothing);
+ });
+
testWidgets('listings page shows load error when venue stream fails', (
tester,
) async {
@@ -378,6 +419,9 @@ void main() {
..listingStreamErrorIds.add('bad-listing');
await _pumpDetail(tester, repo, 'bad-listing');
expect(find.text('Unable to load'), findsOneWidget);
+ await tester.tap(find.widgetWithText(FilledButton, 'Retry'));
+ await tester.pumpAndSettle();
+ expect(repo.watchListingCalledAfterRetry, isTrue);
});
testWidgets('listing detail shows load error when venues stream fails', (
@@ -393,8 +437,56 @@ void main() {
);
await _pumpDetail(tester, repo, created.listingId);
expect(find.text('Unable to load'), findsOneWidget);
+ await tester.tap(find.widgetWithText(FilledButton, 'Retry'));
+ await tester.pumpAndSettle();
+ expect(repo.watchVenuesCalledAfterRetry, isTrue);
});
+ testWidgets(
+ 'listing detail reserve success falls back to navigator when go_router is absent',
+ (tester) async {
+ final repo = _InstrumentedRepository();
+ final created = await repo.createListing(
+ _input(
+ DateTime.now(),
+ venueId: 'taipei-nangang-exhibition-center-hall-1',
+ itemType: 'Fallback flow',
+ ),
+ );
+ await _pumpDetail(tester, repo, created.listingId);
+
+ await tester.tap(find.text('Reserve 1 item'));
+ await tester.pumpAndSettle();
+ await tester.tap(find.byType(CheckboxListTile));
+ await tester.pumpAndSettle();
+ await tester.tap(find.widgetWithText(FilledButton, 'Reserve'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Reservation confirmed'), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ 'listing detail renders badge chips and filters unknown badge id',
+ (tester) async {
+ final now = DateTime.now();
+ final repo = _InstrumentedRepository()
+ ..forcedListings['badge-id'] = _forcedListing(
+ now,
+ id: 'badge-id',
+ status: ListingStatus.active,
+ quantityRemaining: 1,
+ expiresAt: now.add(const Duration(hours: 1)),
+ displayNameOptional: 'Badge Enterprise',
+ enterpriseBadges: const ['verified', 'unknown_badge'],
+ );
+
+ await _pumpDetail(tester, repo, 'badge-id');
+ expect(find.text('Verified enterprise'), findsOneWidget);
+ expect(find.text('unknown_badge'), findsNothing);
+ },
+ );
+
testWidgets(
'listing detail renders reserved, expired and completed statuses',
(tester) async {
diff --git a/test/presentation/browse/my_reservations_page_test.dart b/test/presentation/browse/my_reservations_page_test.dart
index 5db4d59..23f4a5d 100644
--- a/test/presentation/browse/my_reservations_page_test.dart
+++ b/test/presentation/browse/my_reservations_page_test.dart
@@ -1,7 +1,11 @@
+import 'dart:async';
+
import 'package:boxmatch/app/app_scope.dart';
import 'package:boxmatch/features/surplus/data/in_memory_surplus_repository.dart';
import 'package:boxmatch/features/surplus/domain/listing_input.dart';
import 'package:boxmatch/features/surplus/domain/listing_visibility.dart';
+import 'package:boxmatch/features/surplus/domain/reservation.dart';
+import 'package:boxmatch/features/surplus/domain/surplus_exceptions.dart';
import 'package:boxmatch/features/surplus/presentation/browse/my_reservations_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -44,6 +48,36 @@ Future _pumpPage(
await tester.pumpAndSettle();
}
+class _ThrowingListRepository extends InMemorySurplusRepository {
+ @override
+ Future> listRecipientReservations({
+ required String claimerUid,
+ }) async {
+ throw const ValidationException('List failed for test.');
+ }
+}
+
+class _PendingListRepository extends InMemorySurplusRepository {
+ final Completer> completer = Completer>();
+
+ @override
+ Future> listRecipientReservations({
+ required String claimerUid,
+ }) async {
+ return completer.future;
+ }
+}
+
+class _ThrowingCancelRepository extends InMemorySurplusRepository {
+ @override
+ Future cancelReservation({
+ required String reservationId,
+ required String claimerUid,
+ }) async {
+ throw const ValidationException('Cancel failed for test.');
+ }
+}
+
void main() {
testWidgets('shows privacy/faq without client-side inferred badge', (
tester,
@@ -104,6 +138,74 @@ void main() {
await _pumpPage(tester, repo: repo);
expect(find.text('No reservations yet.'), findsOneWidget);
- expect(find.widgetWithText(FilledButton, 'Browse listings'), findsOneWidget);
+ expect(
+ find.widgetWithText(FilledButton, 'Browse listings'),
+ findsOneWidget,
+ );
+ });
+
+ testWidgets('shows loading skeleton while waiting for reservations', (
+ tester,
+ ) async {
+ final repo = _PendingListRepository();
+ final deps = await buildTestDependencies(repository: repo);
+ tester.view.physicalSize = const Size(1280, 2000);
+ tester.view.devicePixelRatio = 1.0;
+ addTearDown(tester.view.resetPhysicalSize);
+ addTearDown(tester.view.resetDevicePixelRatio);
+
+ await tester.pumpWidget(
+ AppScope(
+ dependencies: deps,
+ child: const MaterialApp(home: MyReservationsPage()),
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 200));
+
+ expect(find.byType(ConstrainedBox), findsWidgets);
+ expect(find.byType(Card), findsWidgets);
+ repo.completer.complete(const []);
+ });
+
+ testWidgets('shows error view and warmup hint after retry fails', (
+ tester,
+ ) async {
+ final repo = _ThrowingListRepository();
+ final deps = await buildTestDependencies(repository: repo);
+
+ await tester.pumpWidget(
+ AppScope(
+ dependencies: deps,
+ child: const MaterialApp(home: MyReservationsPage()),
+ ),
+ );
+ await tester.pump(const Duration(milliseconds: 900));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Unable to load'), findsOneWidget);
+ expect(
+ find.textContaining('Service may still be warming up'),
+ findsOneWidget,
+ );
+ });
+
+ testWidgets('cancel reservation error shows snackbar', (tester) async {
+ final repo = _ThrowingCancelRepository();
+ final now = DateTime.now();
+ final listing = await repo.createListing(
+ _input(now, itemType: 'Lunchbox', displayName: 'Acme Charity'),
+ );
+ await repo.reserveListing(
+ listingId: listing.listingId,
+ claimerUid: 'test-user',
+ qty: 1,
+ disclaimerAccepted: true,
+ );
+
+ await _pumpPage(tester, repo: repo);
+ await tester.tap(find.widgetWithText(OutlinedButton, 'Cancel reservation'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Cancel failed for test.'), findsOneWidget);
});
}