MotorSignal-App/test/group_ride_moderation_widge...

448 lines
14 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:motorcycle_dash/core/theme/theme.dart';
import 'package:motorcycle_dash/features/group_rides/group_ride_detail_screen.dart';
import 'package:motorcycle_dash/features/records/records_service.dart';
import 'package:motorcycle_dash/services/auth/auth_providers.dart';
import 'package:motorcycle_dash/services/auth/auth_service.dart';
import 'package:motorcycle_dash/services/auth/auth_state.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/group_rides/group_ride_models.dart';
import 'package:motorcycle_dash/services/group_rides/group_ride_providers.dart';
import 'package:motorcycle_dash/services/group_rides/group_ride_service.dart';
import 'package:motorcycle_dash/services/social/rider_profile_models.dart';
import 'package:motorcycle_dash/services/social/social_providers.dart';
class _FakeAuth implements AuthService {
final AuthUser? user;
_FakeAuth({this.user});
@override
Stream<AuthState> get authStateChanges => Stream.value(
user != null
? AuthState.authenticated(user!, isFirebaseConfigured: true)
: const AuthState.unauthenticated(isFirebaseConfigured: true),
);
@override
AuthState get currentState => user != null
? AuthState.authenticated(user!, isFirebaseConfigured: true)
: const AuthState.unauthenticated(isFirebaseConfigured: true);
@override
AuthUser? get currentUser => user;
@override
bool get isConfigured => true;
@override
bool get isEmailVerified => true;
@override
Future<String?> getIdToken([bool forceRefresh = false]) async => 'mock-id-token';
@override
Future<void> signInWithEmailAndPassword(String email, String password) async {}
@override
Future<void> createUserWithEmailAndPassword(String email, String password, {String? displayName}) async {}
@override
Future<void> signInWithGoogle() async {}
@override
Future<void> signInAnonymously() async {}
@override
Future<void> ensureBackendIdentity() async {}
@override
Future<void> sendPasswordResetEmail(String email) async {}
@override
Future<void> signOut() async {}
@override
void dispose() {}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _RecordingGroupRideService extends Fake implements GroupRideService {
String? lastModeratedCallerUid;
String? lastModeratedGroupRideId;
String? lastModeratedRequesterUid;
bool? lastModeratedApprove;
@override
Future<void> respondToJoinRequest({
required String callerUid,
required String groupRideId,
required bool approve,
required String requesterUid,
}) async {
lastModeratedCallerUid = callerUid;
lastModeratedGroupRideId = groupRideId;
lastModeratedRequesterUid = requesterUid;
lastModeratedApprove = approve;
}
@override
Stream<GroupRideParticipant?> participantDocStream({
required String groupRideId,
required String participantUid,
}) =>
Stream.value(null);
@override
Stream<GroupRide?> groupRideStream(String groupRideId) => Stream.value(null);
@override
Stream<LiveGroupPresence?> livePresenceDocStream({
required String groupRideId,
required String targetUid,
}) =>
Stream.value(null);
@override
Future<void> publishLivePresence({
required String callerUid,
required String groupRideId,
required double latitude,
required double longitude,
double? heading,
double? speedMph,
double? accuracyMeters,
LivePresenceStatus status = LivePresenceStatus.riding,
bool sharingEnabled = true,
}) async {}
@override
Future<void> clearLivePresence({
required String callerUid,
required String groupRideId,
}) async {}
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late SharedPreferences prefs;
final now = DateTime(2026, 9, 24, 10, 0);
final organizerUser = const AuthUser(
uid: 'organizer_1',
email: 'organizer@example.com',
displayName: 'Alice Apex',
isAnonymous: false,
);
final adminUser = const AuthUser(
uid: 'admin_1',
email: 'admin@example.com',
displayName: 'Charlie Admin',
isAnonymous: false,
);
final regularUser = const AuthUser(
uid: 'rider_1',
email: 'rider@example.com',
displayName: 'Bob Rider',
isAnonymous: false,
);
final testRide = GroupRide(
id: 'ride_101',
organizerUid: 'organizer_1',
organizerHandle: 'alice_apex',
organizerDisplayName: 'Alice Apex',
title: 'Blue Ridge Twisties',
description: 'Scenic group ride through Blood Mountain.',
status: GroupRideStatus.scheduled,
joinPolicy: GroupRideJoinPolicy.requestToJoin,
scheduledStartAt: now.add(const Duration(days: 1)),
departureAt: now.add(const Duration(days: 1, minutes: 30)),
participantLimit: 10,
participantCount: 1,
accessUids: const ['organizer_1'],
meetingPoint: const GroupRideLocation(
latitude: 34.5,
longitude: -84.0,
displayName: 'Dahlonega Square',
),
clubId: 'club_77',
createdAt: now,
updatedAt: now,
);
final organizerParticipant = GroupRideParticipant(
userId: 'organizer_1',
role: GroupRideRole.organizer,
status: ParticipantStatus.accepted,
riderDisplayName: 'Alice Apex',
riderHandle: 'alice_apex',
createdAt: now,
updatedAt: now,
);
final testProfile = RiderProfile(
uid: 'organizer_1',
handle: 'alice_apex',
displayName: 'Alice Apex',
createdAt: now,
updatedAt: now,
);
final pendingRequest = GroupRideJoinRequest(
userId: 'requester_42',
riderDisplayName: 'Dave Curve',
riderHandle: 'dave_curves',
createdAt: now,
);
setUp(() async {
SharedPreferences.setMockInitialValues({});
prefs = await SharedPreferences.getInstance();
});
Widget buildScreen({
required AuthUser user,
required GroupRide ride,
required _RecordingGroupRideService mockService,
AsyncValue<List<GroupRideJoinRequest>>? joinRequestsValue,
ClubMember? clubMember,
}) {
return ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
authServiceProvider.overrideWithValue(_FakeAuth(user: user)),
currentUserProvider.overrideWithValue(user),
groupRideServiceProvider.overrideWithValue(mockService),
groupRideStreamProvider(ride.id).overrideWith((ref) => Stream.value(ride)),
groupRideParticipantsProvider(ride.id).overrideWith((ref) => Stream.value([organizerParticipant])),
groupRideLivePresenceProvider(ride.id).overrideWith((ref) => Stream.value(const [])),
myRiderProfileProvider.overrideWith((ref) => Stream.value(testProfile)),
myFriendsProvider.overrideWith((ref) => Stream.value(const [])),
if (ride.clubId != null)
currentMemberProvider(ride.clubId!).overrideWith((ref) => Stream.value(clubMember)),
if (joinRequestsValue != null)
groupRideJoinRequestsProvider(ride.id).overrideWith((ref) => joinRequestsValue.value ?? const [])
else
groupRideJoinRequestsProvider(ride.id).overrideWith((ref) => [pendingRequest]),
],
child: MaterialApp(
theme: AppTheme.darkTheme,
home: GroupRideDetailScreen(rideId: ride.id),
),
);
}
group('GroupRideDetailScreen Join Request Moderation UI Tests', () {
testWidgets('organizer sees JOIN REQUESTS section and pending request details', (tester) async {
final mockService = _RecordingGroupRideService();
await tester.pumpWidget(
buildScreen(
user: organizerUser,
ride: testRide,
mockService: mockService,
),
);
await tester.pumpAndSettle();
expect(find.text('JOIN REQUESTS (1)'), findsOneWidget);
expect(find.text('Dave Curve'), findsOneWidget);
expect(find.text('@dave_curves'), findsOneWidget);
expect(find.byIcon(Icons.check), findsOneWidget);
expect(find.byIcon(Icons.close), findsOneWidget);
expect(find.byIcon(Icons.refresh), findsOneWidget);
});
testWidgets('organizer approves join request invokes service and triggers feedback', (tester) async {
final mockService = _RecordingGroupRideService();
await tester.pumpWidget(
buildScreen(
user: organizerUser,
ride: testRide,
mockService: mockService,
),
);
await tester.pumpAndSettle();
final approveButton = find.byTooltip('Approve request');
expect(approveButton, findsOneWidget);
await tester.tap(approveButton);
await tester.pump();
expect(mockService.lastModeratedCallerUid, equals('organizer_1'));
expect(mockService.lastModeratedGroupRideId, equals('ride_101'));
expect(mockService.lastModeratedRequesterUid, equals('requester_42'));
expect(mockService.lastModeratedApprove, isTrue);
await tester.pumpAndSettle();
expect(find.text('Approved Dave Curve to join the ride.'), findsOneWidget);
});
testWidgets('organizer declines join request invokes service with approve: false', (tester) async {
final mockService = _RecordingGroupRideService();
await tester.pumpWidget(
buildScreen(
user: organizerUser,
ride: testRide,
mockService: mockService,
),
);
await tester.pumpAndSettle();
final declineButton = find.byTooltip('Decline request');
expect(declineButton, findsOneWidget);
await tester.tap(declineButton);
await tester.pump();
expect(mockService.lastModeratedCallerUid, equals('organizer_1'));
expect(mockService.lastModeratedGroupRideId, equals('ride_101'));
expect(mockService.lastModeratedRequesterUid, equals('requester_42'));
expect(mockService.lastModeratedApprove, isFalse);
await tester.pumpAndSettle();
expect(find.text('Declined join request from Dave Curve.'), findsOneWidget);
});
testWidgets('club admin who is not organizer sees moderation controls', (tester) async {
final mockService = _RecordingGroupRideService();
final adminMember = ClubMember(
uid: adminUser.uid,
clubId: testRide.clubId!,
status: ClubMemberStatus.active,
role: ClubRole.admin,
joinedAt: now,
updatedAt: now,
);
await tester.pumpWidget(
buildScreen(
user: adminUser,
ride: testRide,
mockService: mockService,
clubMember: adminMember,
),
);
await tester.pumpAndSettle();
expect(find.text('JOIN REQUESTS (1)'), findsOneWidget);
expect(find.text('Dave Curve'), findsOneWidget);
expect(find.byTooltip('Approve request'), findsOneWidget);
expect(find.byTooltip('Decline request'), findsOneWidget);
});
testWidgets('regular rider does not see JOIN REQUESTS section', (tester) async {
final mockService = _RecordingGroupRideService();
final regularMember = ClubMember(
uid: regularUser.uid,
clubId: testRide.clubId!,
status: ClubMemberStatus.active,
role: ClubRole.member,
joinedAt: now,
updatedAt: now,
);
await tester.pumpWidget(
buildScreen(
user: regularUser,
ride: testRide,
mockService: mockService,
clubMember: regularMember,
),
);
await tester.pumpAndSettle();
expect(find.textContaining('JOIN REQUESTS'), findsNothing);
expect(find.byTooltip('Approve request'), findsNothing);
expect(find.byTooltip('Decline request'), findsNothing);
});
testWidgets('empty requests list does not render section header', (tester) async {
final mockService = _RecordingGroupRideService();
await tester.pumpWidget(
buildScreen(
user: organizerUser,
ride: testRide,
mockService: mockService,
joinRequestsValue: const AsyncValue.data([]),
),
);
await tester.pumpAndSettle();
expect(find.textContaining('JOIN REQUESTS'), findsNothing);
});
testWidgets('displays error message and retry button on fetch failure', (tester) async {
final mockService = _RecordingGroupRideService();
await tester.pumpWidget(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
authServiceProvider.overrideWithValue(_FakeAuth(user: organizerUser)),
currentUserProvider.overrideWithValue(organizerUser),
groupRideServiceProvider.overrideWithValue(mockService),
groupRideStreamProvider(testRide.id).overrideWith((ref) => Stream.value(testRide)),
groupRideParticipantsProvider(testRide.id).overrideWith((ref) => Stream.value([organizerParticipant])),
groupRideLivePresenceProvider(testRide.id).overrideWith((ref) => Stream.value(const [])),
myRiderProfileProvider.overrideWith((ref) => Stream.value(testProfile)),
myFriendsProvider.overrideWith((ref) => Stream.value(const [])),
groupRideJoinRequestsProvider(testRide.id).overrideWith(
(ref) => Future.error(StateError('Server connection error')),
),
],
child: MaterialApp(
theme: AppTheme.darkTheme,
home: GroupRideDetailScreen(rideId: testRide.id),
),
),
);
await tester.pumpAndSettle();
expect(find.text('Unable to load join requests.'), findsOneWidget);
expect(find.text('Retry'), findsOneWidget);
});
testWidgets('canceled ride hides JOIN REQUESTS section even for organizer', (tester) async {
final mockService = _RecordingGroupRideService();
final canceledRide = testRide.copyWith(status: GroupRideStatus.canceled);
await tester.pumpWidget(
buildScreen(
user: organizerUser,
ride: canceledRide,
mockService: mockService,
),
);
await tester.pumpAndSettle();
expect(find.textContaining('JOIN REQUESTS'), findsNothing);
});
});
}