import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/core/theme/theme.dart'; import 'package:motorcycle_dash/features/group_rides/group_rides_hub_screen.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/group_rides/group_ride_models.dart'; import 'package:motorcycle_dash/services/group_rides/group_ride_providers.dart'; import 'package:motorcycle_dash/services/social/rider_profile_models.dart'; import 'package:motorcycle_dash/services/social/social_providers.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; class _FakeAuth implements AuthService { @override Future ensureBackendIdentity() async {} final _states = StreamController.broadcast(); AuthUser? user; _FakeAuth({this.user}); @override Stream get authStateChanges => _states.stream; @override AuthState get currentState => user == null ? const AuthState.unauthenticated(isFirebaseConfigured: true) : (user!.isAnonymous ? AuthState.anonymous(user!, isFirebaseConfigured: true) : AuthState.authenticated(user!, isFirebaseConfigured: true)); @override AuthUser? get currentUser => user; @override bool get isConfigured => true; @override bool get isEmailVerified => true; @override Future deleteCurrentUser() async {} @override void dispose() => _states.close(); @override Future getIdToken([bool forceRefresh = false]) async => 'token'; @override Future reauthenticate({String? password}) async {} @override Future reloadCurrentUser() async {} @override Future sendEmailVerification() async {} @override Future sendPasswordResetEmail(String email) async {} @override Future signInAnonymously() async {} @override Future signInWithEmailAndPassword(String email, String password) async {} @override Future createUserWithEmailAndPassword(String email, String password, {String? displayName}) async {} @override Future signInWithGoogle() async {} @override Future signOut() async {} } Future _captureScreen( WidgetTester tester, GlobalKey repaintKey, String filePath, ) async { await tester.binding.runAsync(() async { final boundary = repaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) { throw StateError('RepaintBoundary not found for capture'); } final image = await boundary.toImage(pixelRatio: 2.0); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); if (byteData == null) { throw StateError('Failed to encode image to PNG'); } final pngBytes = byteData.buffer.asUint8List(); final file = File(filePath); await file.parent.create(recursive: true); await file.writeAsBytes(pngBytes, flush: true); }); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); late SharedPreferences prefs; setUp(() async { SharedPreferences.setMockInitialValues({}); prefs = await SharedPreferences.getInstance(); }); const ownerUser = AuthUser( uid: 'qa-owner-uid', email: 'qa-owner@motorsignal.example', displayName: 'QA Owner', isAnonymous: false, ); const memberUser = AuthUser( uid: 'qa-member-uid', email: 'qa-member@motorsignal.example', displayName: 'QA Member', isAnonymous: false, ); final ownerProfile = RiderProfile( uid: 'qa-owner-uid', handle: 'qa-owner', displayName: 'QA Owner (Ride Lead)', bio: 'Pacesetter and group organizer.', region: 'North Georgia Mountains', avatarPreset: 'preset_neon_cyan', privacySettings: const ProfilePrivacySettings( isSearchableByHandle: true, isProfilePublic: true, allowFriendRequests: true, ), createdAt: DateTime(2026, 1, 1), updatedAt: DateTime(2026, 1, 1), ); final memberProfile = RiderProfile( uid: 'qa-member-uid', handle: 'qa-member', displayName: 'QA Member (Participant)', bio: 'Weekend enthusiast.', region: 'North Georgia Mountains', avatarPreset: 'preset_hyper_purple', privacySettings: const ProfilePrivacySettings( isSearchableByHandle: true, isProfilePublic: true, allowFriendRequests: true, ), createdAt: DateTime(2026, 2, 1), updatedAt: DateTime(2026, 2, 1), ); final now = DateTime(2026, 10, 6, 10, 0); final ride1 = GroupRide( id: 'qa-e2e-join-approval-ride', organizerUid: 'qa-owner-uid', organizerHandle: 'qa-owner', organizerDisplayName: 'QA Owner', title: 'Blood Mountain Sweep', description: 'Scenic group ride through Neels Gap and Vogel State Park.', scheduledStartAt: now.add(const Duration(hours: 2)), departureAt: now.add(const Duration(hours: 2, minutes: 30)), status: GroupRideStatus.scheduled, joinPolicy: GroupRideJoinPolicy.requestToJoin, visibility: GroupRideVisibility.public, participantLimit: 12, participantCount: 3, accessUids: const ['qa-owner-uid', 'qa-member-uid', 'qa-admin-uid'], meetingPoint: const GroupRideLocation( latitude: 34.5268, longitude: -83.9849, displayName: 'Dahlonega Square', address: 'Public Square, Dahlonega, GA 30533', locationType: GroupRideLocationType.meetup, ), destination: const GroupRideLocation( latitude: 34.7644, longitude: -83.9248, displayName: 'Neels Gap / Vogel State Park', address: 'Blairsville, GA', locationType: GroupRideLocationType.destination, ), stops: const [ GroupRideLocation( latitude: 34.6710, longitude: -83.9180, displayName: 'Mountain Crossing Viewpoint', locationType: GroupRideLocationType.scenic, ), ], createdAt: now.subtract(const Duration(days: 1)), updatedAt: now, ); final ride2 = GroupRide( id: 'qa-member-georgia-twisties', organizerUid: 'qa-member-uid', organizerHandle: 'qa-member', organizerDisplayName: 'QA Member', title: 'North Georgia Sunday Twisties', description: 'Scenic sweepers through Suches and Blood Mountain.', scheduledStartAt: now.add(const Duration(days: 1)), departureAt: now.add(const Duration(days: 1, minutes: 30)), status: GroupRideStatus.scheduled, joinPolicy: GroupRideJoinPolicy.open, visibility: GroupRideVisibility.public, participantLimit: 8, participantCount: 2, accessUids: const ['qa-member-uid', 'qa-owner-uid'], meetingPoint: const GroupRideLocation( latitude: 34.6895, longitude: -84.0227, displayName: 'Two Wheels of Suches', address: '1915 GA-60, Suches, GA 30572', locationType: GroupRideLocationType.meetup, ), destination: const GroupRideLocation( latitude: 34.7644, longitude: -83.9248, displayName: 'Vogel State Park', address: 'Blairsville, GA', locationType: GroupRideLocationType.destination, ), stops: const [], createdAt: now.subtract(const Duration(hours: 12)), updatedAt: now, ); const phoneViewport = Size(448, 997); // Pixel 8 Pro logical size group('GroupRidesHubScreen Populated Hub Evidence & Captures', () { testWidgets('Populated Hub - Organizer Account (Night Mode)', (tester) async { tester.view.physicalSize = const Size(1344, 2992); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); final fakeAuth = _FakeAuth(user: ownerUser); final repaintKey = GlobalKey(); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(ownerUser), myRiderProfileProvider.overrideWith( (ref) => Stream.value(ownerProfile), ), upcomingGroupRidesProvider.overrideWith( (ref) => Stream.value([ride1, ride2]), ), myGroupRidesProvider.overrideWith( (ref) => Stream.value([ride1]), ), ], child: MaterialApp( theme: AppTheme.darkTheme, home: RepaintBoundary( key: repaintKey, child: const GroupRidesHubScreen(), ), ), ), ); await tester.pumpAndSettle(); expect(find.text('GROUP RIDES'), findsOneWidget); expect(find.text('@qa-owner'), findsOneWidget); expect(find.text('Blood Mountain Sweep'), findsOneWidget); expect(find.text('North Georgia Sunday Twisties'), findsOneWidget); expect(tester.takeException(), isNull); await _captureScreen( tester, repaintKey, 'docs/design/motorsignal-hybrid-v1/screenshots/checkpoint_25_hub_organizer_night.png', ); }); testWidgets('Populated Hub - Organizer Account (Day Mode)', (tester) async { tester.view.physicalSize = const Size(1344, 2992); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); final fakeAuth = _FakeAuth(user: ownerUser); final repaintKey = GlobalKey(); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(ownerUser), myRiderProfileProvider.overrideWith( (ref) => Stream.value(ownerProfile), ), upcomingGroupRidesProvider.overrideWith( (ref) => Stream.value([ride1, ride2]), ), myGroupRidesProvider.overrideWith( (ref) => Stream.value([ride1]), ), ], child: MaterialApp( theme: AppTheme.lightTheme, home: RepaintBoundary( key: repaintKey, child: const GroupRidesHubScreen(), ), ), ), ); await tester.pumpAndSettle(); expect(find.text('GROUP RIDES'), findsOneWidget); expect(find.text('Blood Mountain Sweep'), findsOneWidget); expect(tester.takeException(), isNull); await _captureScreen( tester, repaintKey, 'docs/design/motorsignal-hybrid-v1/screenshots/checkpoint_26_hub_organizer_day.png', ); }); testWidgets('Populated Hub - Organizer Account (Large Text 200% Scale)', (tester) async { tester.view.physicalSize = const Size(1344, 2992); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); final fakeAuth = _FakeAuth(user: ownerUser); final repaintKey = GlobalKey(); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(ownerUser), myRiderProfileProvider.overrideWith( (ref) => Stream.value(ownerProfile), ), upcomingGroupRidesProvider.overrideWith( (ref) => Stream.value([ride1, ride2]), ), myGroupRidesProvider.overrideWith( (ref) => Stream.value([ride1]), ), ], child: MediaQuery( data: const MediaQueryData( size: phoneViewport, textScaler: TextScaler.linear(2.0), ), child: MaterialApp( theme: AppTheme.darkTheme, home: RepaintBoundary( key: repaintKey, child: const GroupRidesHubScreen(), ), ), ), ), ); await tester.pumpAndSettle(); expect(find.text('GROUP RIDES'), findsOneWidget); expect(find.text('Blood Mountain Sweep'), findsOneWidget); final exception = tester.takeException(); if (exception is FlutterError) { debugPrint(exception.toStringDeep()); } expect(exception, isNull); await _captureScreen( tester, repaintKey, 'docs/design/motorsignal-hybrid-v1/screenshots/checkpoint_27_hub_organizer_large_text.png', ); }); testWidgets('Populated Hub - Participant Account (Night Mode)', (tester) async { tester.view.physicalSize = const Size(1344, 2992); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); final fakeAuth = _FakeAuth(user: memberUser); final repaintKey = GlobalKey(); await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(fakeAuth), currentUserProvider.overrideWithValue(memberUser), myRiderProfileProvider.overrideWith( (ref) => Stream.value(memberProfile), ), upcomingGroupRidesProvider.overrideWith( (ref) => Stream.value([ride1, ride2]), ), myGroupRidesProvider.overrideWith( (ref) => Stream.value([ride2]), ), ], child: MaterialApp( theme: AppTheme.darkTheme, home: RepaintBoundary( key: repaintKey, child: const GroupRidesHubScreen(), ), ), ), ); await tester.pumpAndSettle(); expect(find.text('GROUP RIDES'), findsOneWidget); expect(find.text('@qa-member'), findsOneWidget); expect(find.text('Blood Mountain Sweep'), findsOneWidget); expect(tester.takeException(), isNull); await _captureScreen( tester, repaintKey, 'docs/design/motorsignal-hybrid-v1/screenshots/checkpoint_28_hub_participant.png', ); }); }); }