MotorSignal-App/test/clubs_hub_screen_test.dart

328 lines
11 KiB
Dart

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<String, Club> clubs;
final Map<String, String> handles;
FakeClubServiceForHub({
this.clubs = const {},
this.handles = const {},
});
@override
Future<Club?> getClubBySlug(String slug) async {
final clubId = handles[slug];
if (clubId == null) return null;
return clubs[clubId];
}
@override
Future<List<Club>> 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);
}
});
}
});
}