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); }); }