diff --git a/scripts/seed_verified_enterprises.js b/scripts/seed_verified_enterprises.js index 4c1b49d..d2d562a 100644 --- a/scripts/seed_verified_enterprises.js +++ b/scripts/seed_verified_enterprises.js @@ -2,7 +2,9 @@ const fs = require('fs'); const path = require('path'); -const admin = require('firebase-admin'); +const { createRequire } = require('module'); +const requireFromCwd = createRequire(path.join(process.cwd(), 'package.json')); +const admin = requireFromCwd('firebase-admin'); function fail(message) { console.error(message); @@ -38,7 +40,7 @@ function initAdminFromEnv() { async function main() { const inputPath = process.argv[2] || - path.resolve(process.cwd(), 'docs/ops/verified_enterprises.seed.json'); + path.resolve(__dirname, '../docs/ops/verified_enterprises.seed.json'); if (!fs.existsSync(inputPath)) { fail(`Seed file not found: ${inputPath}`); diff --git a/test/core/app_strings_test.dart b/test/core/app_strings_test.dart index 48a5df9..1012dc5 100644 --- a/test/core/app_strings_test.dart +++ b/test/core/app_strings_test.dart @@ -36,6 +36,74 @@ void main() { expect(strings.highImpactEnterprise, 'High-impact donor'); expect(strings.flexiblePickupEnterprise, contains('Flexible')); expect(strings.stableShelfLifeEnterprise, contains('Stable')); + expect(strings.appTitle, isNotEmpty); + expect(strings.navMap, 'Map'); + expect(strings.navPost, 'Post'); + expect(strings.listingsTitle, isNotEmpty); + expect(strings.refresh, 'Refresh'); + expect(strings.privateDonor, isNotEmpty); + expect(strings.noActiveListings, contains('No active listings')); + expect(strings.localDemoModeNotice, contains('local demo mode')); + expect(strings.platformDisclaimer, contains('matching service')); + expect(strings.mapTitle, 'Venue Map'); + expect(strings.activeCount(3), '3 active'); + expect(strings.listingDetailTitle, isNotEmpty); + expect(strings.myReservationsTitle, isNotEmpty); + expect(strings.myReservationsCta, isNotEmpty); + expect(strings.noMyReservations, isNotEmpty); + expect(strings.cancelReservation, isNotEmpty); + expect(strings.reservationCancelled, isNotEmpty); + expect(strings.listingNotFound, isNotEmpty); + expect(strings.reserveOneItem, isNotEmpty); + expect(strings.beforeReserving, isNotEmpty); + expect(strings.reserveDisclaimer, isNotEmpty); + expect(strings.publicPickupOnlyNotice, isNotEmpty); + expect(strings.reserveDisclaimerAccept, isNotEmpty); + expect(strings.cancel, 'Cancel'); + expect(strings.reserve, 'Reserve'); + expect(strings.enterprisePostTitle, isNotEmpty); + expect(strings.enterpriseEditTitle, isNotEmpty); + expect(strings.reservationSection, isNotEmpty); + expect(strings.noReservationsYet, isNotEmpty); + expect(strings.reservationConfirmed, isNotEmpty); + expect(strings.reservationNotFound, isNotEmpty); + expect(strings.offlineIdentityMode, isNotEmpty); + expect(strings.reportSafetyConcern, isNotEmpty); + expect(strings.riskReasonPrivateLocation, isNotEmpty); + expect(strings.riskReasonNoShow, isNotEmpty); + expect(strings.riskReasonUnsafeCondition, isNotEmpty); + expect(strings.riskReasonOther, isNotEmpty); + expect(strings.abuseReported, isNotEmpty); + expect(strings.verifiedEnterprise, isNotEmpty); + expect(strings.trustedQualityEnterprise, isNotEmpty); + expect(strings.pendingConfirm, isNotEmpty); + expect(strings.confirmedFilter, isNotEmpty); + expect(strings.showPickupCodeHelp, isNotEmpty); + expect(strings.retry, isNotEmpty); + expect(strings.genericLoadErrorTitle, isNotEmpty); + expect(strings.genericLoadErrorBody, isNotEmpty); + expect(strings.statusLabel(AppStatusLabel.active), 'Active'); + expect(strings.statusLabel(AppStatusLabel.reserved), 'Reserved'); + expect(strings.statusLabel(AppStatusLabel.expired), 'Expired'); + expect(strings.statusLabel(AppStatusLabel.cancelled), 'Cancelled'); + expect(strings.enterpriseBadgeLabel('verified'), strings.verifiedEnterprise); + expect( + strings.enterpriseBadgeLabel('quality_trusted'), + strings.trustedQualityEnterprise, + ); + expect( + strings.enterpriseBadgeLabel('high_impact'), + strings.highImpactEnterprise, + ); + expect( + strings.enterpriseBadgeLabel('flexible_pickup'), + strings.flexiblePickupEnterprise, + ); + expect( + strings.enterpriseBadgeLabel('stable_shelf_life'), + strings.stableShelfLifeEnterprise, + ); + expect(strings.enterpriseBadgeLabel('unknown_badge'), isNull); }); testWidgets('app strings returns zh-TW labels', (tester) async { @@ -68,5 +136,19 @@ void main() { expect(find.text('隱私與常見問題'), findsOneWidget); expect(find.text('請選擇回報原因'), findsOneWidget); expect(find.text('高量捐贈企業'), findsOneWidget); + await tester.pumpWidget( + AppScope( + dependencies: dependencies, + child: MaterialApp( + home: Builder( + builder: (context) { + final strings = AppStrings.of(context); + return Text(strings.statusLabel(AppStatusLabel.cancelled)); + }, + ), + ), + ), + ); + expect(find.text('已取消'), findsOneWidget); }); } diff --git a/test/presentation/browse/browse_pages_additional_test.dart b/test/presentation/browse/browse_pages_additional_test.dart index 7a51d6b..6f08d41 100644 --- a/test/presentation/browse/browse_pages_additional_test.dart +++ b/test/presentation/browse/browse_pages_additional_test.dart @@ -151,6 +151,7 @@ Listing _forcedListing( required DateTime expiresAt, String? displayNameOptional, bool enterpriseVerified = false, + List enterpriseBadges = const [], }) { return Listing( id: id, @@ -167,6 +168,7 @@ Listing _forcedListing( expiresAt: expiresAt, displayNameOptional: displayNameOptional, enterpriseVerified: enterpriseVerified, + enterpriseBadges: enterpriseBadges, visibility: ListingVisibility.minimal, status: status, editTokenHash: 'hash', @@ -224,6 +226,11 @@ Future _pumpConfirmation( required String reservationId, RecipientIdentityService? identityService, }) async { + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(1200, 2400); + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + final dependencies = await buildTestDependencies( repository: repo, identityService: identityService, @@ -588,4 +595,135 @@ void main() { }, ); + testWidgets( + 'reservation confirmation opens and cancels risk reason dialog', + (tester) async { + final now = DateTime.now(); + final repo = _InstrumentedRepository() + ..forcedListings['reason-dialog'] = _forcedListing( + now, + id: 'reason-dialog', + status: ListingStatus.active, + quantityRemaining: 1, + expiresAt: now.add(const Duration(hours: 2)), + ) + ..forcedReservations['reason-dialog-r'] = _forcedReservation( + now, + id: 'reason-dialog-r', + listingId: 'reason-dialog', + status: ReservationStatus.reserved, + ); + + await _pumpConfirmation( + tester, + repo, + listingId: 'reason-dialog', + reservationId: 'reason-dialog-r', + ); + + final reportButton = find.byIcon(Icons.report_gmailerrorred_outlined); + await tester.scrollUntilVisible( + reportButton, + 200, + scrollable: find.byType(Scrollable).first, + ); + await tester.tap(reportButton); + await tester.pumpAndSettle(); + + expect(find.text('Select a reason'), findsOneWidget); + await tester.tap(find.widgetWithText(TextButton, 'Cancel')); + await tester.pumpAndSettle(); + expect(find.text('Select a reason'), findsNothing); + }, + ); + + testWidgets( + 'reservation confirmation reports selected risk reason and shows badges', + (tester) async { + final now = DateTime.now(); + final repo = _InstrumentedRepository() + ..forcedListings['reason-submit'] = _forcedListing( + now, + id: 'reason-submit', + status: ListingStatus.active, + quantityRemaining: 1, + expiresAt: now.add(const Duration(hours: 2)), + enterpriseBadges: const ['verified', 'high_impact'], + ) + ..forcedReservations['reason-submit-r'] = _forcedReservation( + now, + id: 'reason-submit-r', + listingId: 'reason-submit', + status: ReservationStatus.reserved, + ); + + await _pumpConfirmation( + tester, + repo, + listingId: 'reason-submit', + reservationId: 'reason-submit-r', + ); + + expect(find.text('Verified enterprise'), findsOneWidget); + expect(find.text('High-impact donor'), findsOneWidget); + + final reportButton = find.byIcon(Icons.report_gmailerrorred_outlined); + await tester.scrollUntilVisible( + reportButton, + 200, + scrollable: find.byType(Scrollable).first, + ); + await tester.tap(reportButton); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Suspicious behavior / harassment')); + await tester.pumpAndSettle(); + + expect(repo.lastAbuseReason, 'recipient_report_suspicious_behavior'); + expect(find.text('Safety report submitted.'), findsOneWidget); + }, + ); + + testWidgets( + 'reservation confirmation shows error when abuse report fails', + (tester) async { + final now = DateTime.now(); + final repo = _InstrumentedRepository() + ..throwOnAbuseSignal = true + ..forcedListings['reason-fail'] = _forcedListing( + now, + id: 'reason-fail', + status: ListingStatus.active, + quantityRemaining: 1, + expiresAt: now.add(const Duration(hours: 2)), + ) + ..forcedReservations['reason-fail-r'] = _forcedReservation( + now, + id: 'reason-fail-r', + listingId: 'reason-fail', + status: ReservationStatus.reserved, + ); + + await _pumpConfirmation( + tester, + repo, + listingId: 'reason-fail', + reservationId: 'reason-fail-r', + ); + + final reportButton = find.byIcon(Icons.report_gmailerrorred_outlined); + await tester.scrollUntilVisible( + reportButton, + 200, + scrollable: find.byType(Scrollable).first, + ); + await tester.tap(reportButton); + await tester.pumpAndSettle(); + await tester.tap(find.text('Other risk')); + await tester.pumpAndSettle(); + + expect(find.text('Abuse report failed for test.'), findsOneWidget); + }, + ); + }