import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/core/theme/theme_extensions.dart'; import 'package:motorcycle_dash/features/social/clubs/clubs_hub_screen.dart'; import 'package:motorcycle_dash/services/auth/auth_providers.dart'; import 'package:motorcycle_dash/services/auth/auth_user.dart'; import 'package:motorcycle_dash/services/clubs/club_models.dart'; import 'package:motorcycle_dash/services/clubs/club_providers.dart'; import 'package:motorcycle_dash/services/clubs/club_service.dart'; class FakeClubServiceForHub extends ClubService { final Map clubs; final Map handles; FakeClubServiceForHub({ this.clubs = const {}, this.handles = const {}, }); @override Future getClubBySlug(String slug) async { final clubId = handles[slug]; if (clubId == null) return null; return clubs[clubId]; } @override Future> discoverPublicClubs({int limit = 25, dynamic startAfter}) async { return clubs.values.where((c) => c.visibility == ClubVisibility.public).toList(); } } void main() { group('ClubsHubScreen Widget Tests', () { testWidgets('renders account-required notice when signed out', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => null), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubsHubScreen(), ), ), ); expect(find.text('Account Required for Clubs'), findsOneWidget); expect( find.text('Sign in or create a MotorSignal account to join and manage riding clubs.'), findsOneWidget, ); expect(find.text('Sign In / Register'), findsOneWidget); }); testWidgets('renders tabs, empty my clubs, and public clubs when authenticated', (tester) async { const user = AuthUser(uid: 'alice', isAnonymous: false); final publicClub = Club( id: 'club_pub_1', name: 'Bay Area Motos', slug: 'bay-area-motos', description: 'San Francisco & Bay Area motorcycle rides.', ownerUid: 'bob', memberCount: 15, createdAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); final fakeService = FakeClubServiceForHub( clubs: {'club_pub_1': publicClub}, handles: {'bay-area-motos': 'club_pub_1'}, ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => user), clubServiceProvider.overrideWithValue(fakeService), myClubsProvider.overrideWith((ref) => Stream.value(const [])), publicClubsProvider.overrideWith((ref) => Future.value([publicClub])), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubsHubScreen(), ), ), ); await tester.pumpAndSettle(); // Verify tabs expect(find.text('MY CLUBS'), findsOneWidget); expect(find.text('DISCOVER'), findsOneWidget); // Verify Empty My Clubs state expect(find.text('No Club Memberships Yet'), findsOneWidget); expect(find.text('Discover Clubs'), findsOneWidget); // Tap on DISCOVER tab await tester.tap(find.text('DISCOVER')); await tester.pumpAndSettle(); // Verify Public Discovery content expect(find.text('Public Clubs Directory'), findsOneWidget); expect(find.text('Bay Area Motos'), findsOneWidget); expect(find.text('@bay-area-motos'), findsOneWidget); // Verify search input expect(find.byType(TextField), findsOneWidget); }); testWidgets('executes exact handle search in Discover tab', (tester) async { const user = AuthUser(uid: 'alice', isAnonymous: false); final targetClub = Club( id: 'club_norcal', name: 'NorCal Riders', slug: 'norcal-riders', description: 'Northern California twisties.', ownerUid: 'charlie', memberCount: 8, createdAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); final fakeService = FakeClubServiceForHub( clubs: {'club_norcal': targetClub}, handles: {'norcal-riders': 'club_norcal'}, ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => user), clubServiceProvider.overrideWithValue(fakeService), myClubsProvider.overrideWith((ref) => Stream.value(const [])), publicClubsProvider.overrideWith((ref) => Future.value(const [])), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubsHubScreen(), ), ), ); await tester.pumpAndSettle(); // Switch to Discover tab await tester.tap(find.text('DISCOVER')); await tester.pumpAndSettle(); // Search for club by slug await tester.enterText(find.byType(TextField), 'norcal-riders'); await tester.testTextInput.receiveAction(TextInputAction.search); await tester.pumpAndSettle(); expect(find.text('EXACT MATCH: @norcal-riders'), findsOneWidget); expect(find.text('NorCal Riders'), findsOneWidget); // Search for non-existent slug await tester.enterText(find.byType(TextField), 'unknown-club'); await tester.testTextInput.receiveAction(TextInputAction.search); await tester.pumpAndSettle(); expect(find.text('No club found with handle @unknown-club'), findsOneWidget); }); testWidgets('renders incoming invitations section in My Clubs tab and handles accept', (tester) async { const user = AuthUser(uid: 'alice', isAnonymous: false); final inviteClub = Club( id: 'club_invite_1', name: 'Bay Area Motos', slug: 'bay-area-motos', description: 'San Francisco & Bay Area motorcycle rides.', ownerUid: 'bob', memberCount: 15, createdAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); final inviteMember = ClubMember( uid: 'alice', clubId: 'club_invite_1', status: ClubMemberStatus.invited, role: ClubRole.member, invitedByUid: 'bob', updatedAt: DateTime(2026, 1, 1), ); final fakeService = FakeClubServiceForHub( clubs: {'club_invite_1': inviteClub}, handles: {'bay-area-motos': 'club_invite_1'}, ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => user), clubServiceProvider.overrideWithValue(fakeService), clubDetailProvider('club_invite_1').overrideWith((ref) => Stream.value(inviteClub)), myClubsProvider.overrideWith((ref) => Stream.value(const [])), myClubInvitesProvider.overrideWith((ref) => Stream.value([inviteMember])), publicClubsProvider.overrideWith((ref) => Future.value([inviteClub])), ], child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubsHubScreen(), ), ), ); await tester.pumpAndSettle(); expect(find.text('CLUB INVITATIONS'), findsOneWidget); expect(find.text('Bay Area Motos'), findsOneWidget); expect(find.text('INVITED'), findsOneWidget); expect(find.text('Accept'), findsOneWidget); expect(find.text('Decline'), findsOneWidget); }); for (final size in const [Size(800, 360), Size(640, 360), Size(400, 800)]) { testWidgets('renders responsive layout without overflow on $size (#73)', (tester) async { await tester.binding.setSurfaceSize(size); addTearDown(() => tester.binding.setSurfaceSize(null)); const user = AuthUser(uid: 'alice', isAnonymous: false); final club1 = Club( id: 'club_1', name: 'Bay Area Motos', slug: 'bay-area-motos', description: 'San Francisco & Bay Area motorcycle rides.', ownerUid: 'alice', memberCount: 15, admissionPolicy: ClubAdmissionPolicy.open, createdAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); final club2 = Club( id: 'club_2', name: 'NorCal Twisties', slug: 'norcal-twisties', description: 'Carving Northern California canyon roads.', ownerUid: 'bob', memberCount: 24, admissionPolicy: ClubAdmissionPolicy.request, createdAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); final member1 = ClubMember( uid: 'alice', clubId: 'club_1', status: ClubMemberStatus.active, role: ClubRole.owner, updatedAt: DateTime(2026, 1, 1), ); final member2 = ClubMember( uid: 'alice', clubId: 'club_2', status: ClubMemberStatus.active, role: ClubRole.member, updatedAt: DateTime(2026, 1, 1), ); final fakeService = FakeClubServiceForHub( clubs: {'club_1': club1, 'club_2': club2}, handles: {'bay-area-motos': 'club_1', 'norcal-twisties': 'club_2'}, ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWith((ref) => user), clubServiceProvider.overrideWithValue(fakeService), clubDetailProvider('club_1').overrideWith((ref) => Stream.value(club1)), clubDetailProvider('club_2').overrideWith((ref) => Stream.value(club2)), myClubsProvider.overrideWith((ref) => Stream.value([member1, member2])), myClubInvitesProvider.overrideWith((ref) => Stream.value(const [])), publicClubsProvider.overrideWith((ref) => Future.value([club1, club2])), ], child: MediaQuery( data: MediaQueryData(size: size), child: MaterialApp( theme: ThemeData.dark().copyWith( extensions: [MotorSignalThemeColors.dark], ), home: const ClubsHubScreen(), ), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); final isLandscape = size.width > size.height; if (isLandscape) { // Verify GridView is used for memberships in landscape expect(find.byType(GridView), findsOneWidget); } else { expect(find.byType(GridView), findsNothing); } // Tap Discover Tab await tester.tap(find.text('DISCOVER')); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); if (isLandscape) { // Verify GridView is used for discovery in landscape expect(find.byType(GridView), findsOneWidget); } else { expect(find.byType(GridView), findsNothing); } }); } }); }