import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/features/dashboard/dashboard_screen.dart'; import 'package:motorcycle_dash/features/group_rides/live_group_ride_screen.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_service.dart'; import 'package:motorcycle_dash/services/auth/auth_user.dart'; import 'package:motorcycle_dash/services/auth/auth_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/models/telemetry_sample.dart'; import 'package:motorcycle_dash/services/location_service.dart'; import 'package:motorcycle_dash/services/map/unified_map_view.dart'; import 'package:motorcycle_dash/services/telemetry_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/features/rides/ride_state_machine.dart'; class _FakeRideStateMachine extends StateNotifier implements RideStateMachine { _FakeRideStateMachine(super.initialState); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); late SharedPreferences prefs; setUp(() async { SharedPreferences.setMockInitialValues({}); prefs = await SharedPreferences.getInstance(); }); group('LiveGroupRideScreen Widget Tests', () { final now = DateTime.now(); final testRide = GroupRide( id: 'ride_live_test', organizerUid: 'alice', organizerHandle: 'alice_apex', organizerDisplayName: 'Alice Apex', title: 'Blood Mountain Run', status: GroupRideStatus.underway, scheduledStartAt: now, departureAt: now, participantCount: 3, meetingPoint: const GroupRideLocation( latitude: 34.6, longitude: -83.9, displayName: 'Dahlonega Square', ), destination: const GroupRideLocation( latitude: 34.8, longitude: -83.7, displayName: 'Neels Gap', ), stops: const [ GroupRideLocation( latitude: 34.7, longitude: -83.8, displayName: 'Turner Corner Fuel', ), ], createdAt: now, updatedAt: now, ); final participants = [ GroupRideParticipant( userId: 'alice', role: GroupRideRole.lead, status: ParticipantStatus.accepted, riderHandle: 'alice_apex', riderDisplayName: 'Alice Apex', createdAt: now, updatedAt: now, ), GroupRideParticipant( userId: 'bob', role: GroupRideRole.sweep, status: ParticipantStatus.accepted, riderHandle: 'bob_rider', riderDisplayName: 'Bob Rider', createdAt: now, updatedAt: now, ), GroupRideParticipant( userId: 'charlie', role: GroupRideRole.participant, status: ParticipantStatus.accepted, riderHandle: 'charlie_twist', riderDisplayName: 'Charlie Twist', createdAt: now, updatedAt: now, ), ]; final presences = [ LiveGroupPresence( userId: 'alice', groupRideId: 'ride_live_test', latitude: 34.65, longitude: -83.85, heading: 45.0, speedMph: 45.0, accuracyMeters: 5.0, updatedAt: now, expiresAt: now.add(const Duration(seconds: 120)), status: LivePresenceStatus.riding, sharingEnabled: true, ), LiveGroupPresence( userId: 'bob', groupRideId: 'ride_live_test', latitude: 34.64, longitude: -83.86, heading: 45.0, speedMph: 42.0, accuracyMeters: 5.0, updatedAt: now.subtract(const Duration(seconds: 10)), expiresAt: now.add(const Duration(seconds: 110)), status: LivePresenceStatus.needFuel, sharingEnabled: true, ), ]; testWidgets('renders map screen with title, status, and quick status bar', (tester) async { const user = AuthUser( uid: 'alice', email: 'alice@example.com', isAnonymous: false, ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), groupRideStreamProvider('ride_live_test') .overrideWith((ref) => Stream.value(testRide)), groupRideParticipantsProvider('ride_live_test') .overrideWith((ref) => Stream.value(participants)), groupRideLivePresenceProvider('ride_live_test') .overrideWith((ref) => Stream.value(presences)), groupRideLiveSharingAllowedProvider.overrideWith((ref) => true), ], child: const MaterialApp( home: LiveGroupRideScreen(rideId: 'ride_live_test'), ), ), ); await tester.pumpAndSettle(); expect(find.text('BLOOD MOUNTAIN RUN'), findsOneWidget); expect(find.text('UNDERWAY'), findsOneWidget); // Quick Status bar items expect(find.text('RIDING'), findsWidgets); expect(find.text('STOPPED'), findsOneWidget); expect(find.text('NEED FUEL'), findsWidgets); expect(find.text('REGROUP'), findsOneWidget); expect(find.text('DELAYED'), findsOneWidget); expect(find.text('ARRIVED'), findsOneWidget); }); testWidgets('displays regroup banner when regrouping is active', (tester) async { const user = AuthUser( uid: 'alice', email: 'alice@example.com', isAnonymous: false, ); final regroupPresences = [ LiveGroupPresence( userId: 'bob', groupRideId: 'ride_live_test', latitude: 34.64, longitude: -83.86, heading: 45.0, speedMph: 0.0, accuracyMeters: 5.0, updatedAt: now, expiresAt: now.add(const Duration(seconds: 120)), status: LivePresenceStatus.regrouping, sharingEnabled: true, ), ]; await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), groupRideStreamProvider('ride_live_test') .overrideWith((ref) => Stream.value(testRide)), groupRideParticipantsProvider('ride_live_test') .overrideWith((ref) => Stream.value(participants)), groupRideLivePresenceProvider('ride_live_test') .overrideWith((ref) => Stream.value(regroupPresences)), groupRideLiveSharingAllowedProvider.overrideWith((ref) => true), ], child: const MaterialApp( home: LiveGroupRideScreen(rideId: 'ride_live_test'), ), ), ); await tester.pumpAndSettle(); expect(find.text('REGROUP CALLED'), findsOneWidget); expect(find.textContaining('Bob Rider'), findsOneWidget); }); testWidgets('shows live roster sheet with lead and separation distance', (tester) async { const user = AuthUser( uid: 'bob', email: 'bob@example.com', isAnonymous: false, ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), groupRideStreamProvider('ride_live_test') .overrideWith((ref) => Stream.value(testRide)), groupRideParticipantsProvider('ride_live_test') .overrideWith((ref) => Stream.value(participants)), groupRideLivePresenceProvider('ride_live_test') .overrideWith((ref) => Stream.value(presences)), groupRideLiveSharingAllowedProvider.overrideWith((ref) => true), ], child: const MaterialApp( home: LiveGroupRideScreen(rideId: 'ride_live_test'), ), ), ); await tester.pumpAndSettle(); // Open Roster Bottom Sheet final rosterButton = find.byIcon(Icons.format_list_bulleted); expect(rosterButton, findsOneWidget); await tester.tap(rosterButton); await tester.pumpAndSettle(); expect(find.text('LIVE RIDER ROSTER'), findsOneWidget); expect(find.text('Alice Apex'), findsOneWidget); expect(find.text('RIDE LEAD'), findsWidgets); expect(find.text('Bob Rider'), findsOneWidget); expect(find.text('SWEEP'), findsOneWidget); expect(find.text('Charlie Twist'), findsOneWidget); expect(find.text('OFFLINE'), findsWidgets); }); testWidgets('responsive layout on 320dp width without overflow', (tester) async { tester.view.physicalSize = const Size(320 * 2.0, 640 * 2.0); tester.view.devicePixelRatio = 2.0; addTearDown(() => tester.view.resetPhysicalSize()); const user = AuthUser( uid: 'alice', email: 'alice@example.com', isAnonymous: false, ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), groupRideStreamProvider('ride_live_test') .overrideWith((ref) => Stream.value(testRide)), groupRideParticipantsProvider('ride_live_test') .overrideWith((ref) => Stream.value(participants)), groupRideLivePresenceProvider('ride_live_test') .overrideWith((ref) => Stream.value(presences)), groupRideLiveSharingAllowedProvider.overrideWith((ref) => true), ], child: const MaterialApp( home: LiveGroupRideScreen(rideId: 'ride_live_test'), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(find.text('BLOOD MOUNTAIN RUN'), findsOneWidget); expect(find.byType(UnifiedMapView), findsOneWidget); }); testWidgets('dashboard displays active group ride banner when ride is active', (tester) async { await tester.pumpWidget( ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), rideStateMachineProvider.overrideWith( (ref) => _FakeRideStateMachine(RideSession()), ), activeUserGroupRideProvider.overrideWithValue(testRide), telemetryStreamProvider.overrideWith( (ref) => Stream.value(TelemetrySample( timestamp: DateTime.now(), speed: 0.0, heading: 0.0, leanAngle: 0.0, ))), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])), locationPermissionGrantedProvider .overrideWith((ref) => Future.value(true)), ], child: const MaterialApp( home: DashboardScreen(), ), ), ); await tester.pumpAndSettle(); expect( find.textContaining('GROUP RIDE ACTIVE: BLOOD MOUNTAIN RUN'), findsOneWidget, ); expect(find.text('3 RIDERS'), findsOneWidget); }); for (final size in const [Size(800, 360), Size(640, 360), Size(400, 800)]) { testWidgets('renders landscape controls and panel without overflow on $size (#74)', (tester) async { await tester.binding.setSurfaceSize(size); addTearDown(() => tester.binding.setSurfaceSize(null)); const user = AuthUser( uid: 'alice', email: 'alice@example.com', isAnonymous: false, ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), groupRideStreamProvider('ride_live_test') .overrideWith((ref) => Stream.value(testRide)), groupRideParticipantsProvider('ride_live_test') .overrideWith((ref) => Stream.value(participants)), groupRideLivePresenceProvider('ride_live_test') .overrideWith((ref) => Stream.value(presences)), groupRideLiveSharingAllowedProvider.overrideWith((ref) => true), ], child: MediaQuery( data: MediaQueryData(size: size), child: const MaterialApp( home: LiveGroupRideScreen(rideId: 'ride_live_test'), ), ), ), ); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); // Verify key controls are present and reachable expect( find.byKey(const ValueKey('live_ride_broadcast_switch')), findsOneWidget, ); expect( find.byKey(const ValueKey('live_ride_recenter_button')), findsOneWidget, ); expect( find.byKey(const ValueKey('live_ride_roster_button')), findsOneWidget, ); // Status bar items expect(find.text('RIDING'), findsWidgets); expect(find.text('STOPPED'), findsOneWidget); expect(find.text('NEED FUEL'), findsWidgets); expect(find.text('REGROUP'), findsOneWidget); final isLandscape = size.width > size.height; if (isLandscape) { // In landscape, verify side-by-side pack telemetry stream panel is present expect(find.text('PACK TELEMETRY STREAM'), findsOneWidget); } }); } }); }