import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; // ignore_for_file: unused_local_variable, avoid_print import 'dart:async'; import 'package:motorcycle_dash/features/group_rides/create_group_ride_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/group_rides/group_ride_models.dart'; import 'package:motorcycle_dash/services/group_rides/group_ride_service.dart'; import 'package:motorcycle_dash/services/group_rides/group_ride_providers.dart'; import 'package:motorcycle_dash/services/routes/route_models.dart'; import 'package:motorcycle_dash/services/routes/route_provider.dart'; import 'package:motorcycle_dash/services/routes/route_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/routes/widgets/address_autocomplete_field.dart'; import 'package:motorcycle_dash/services/clubs/club_models.dart'; import 'package:motorcycle_dash/services/clubs/club_providers.dart'; import 'fake_route_provider.dart'; class _MockGroupRideService extends GroupRideService { GroupRide? createdRide; @override Future createGroupRide({ required RiderProfile organizer, required String title, String description = '', GroupRideVisibility visibility = GroupRideVisibility.friendsOnly, GroupRideJoinPolicy joinPolicy = GroupRideJoinPolicy.inviteOnly, required DateTime scheduledStartAt, required DateTime departureAt, int? participantLimit, required GroupRideLocation meetingPoint, GroupRideLocation? destination, List stops = const [], List initialInviteUids = const [], String? routeId, String? routeTitle, double? routeDistanceMeters, int? routeStopCount, String? clubId, }) async { final now = DateTime.now(); createdRide = GroupRide( id: 'ride_new_test', organizerUid: organizer.uid, organizerHandle: organizer.handle, organizerDisplayName: organizer.displayName, title: title, description: description, visibility: visibility, joinPolicy: joinPolicy, scheduledStartAt: scheduledStartAt, departureAt: departureAt, participantLimit: participantLimit, meetingPoint: meetingPoint, destination: destination, stops: stops, routeId: routeId, routeTitle: routeTitle, clubId: clubId, createdAt: now, updatedAt: now, ); return createdRide!; } } void main() { group('CreateGroupRideScreen Route-P2 Tests', () { late RiderProfile profile; late AuthUser user; setUp(() { final now = DateTime.now(); profile = RiderProfile( uid: 'alice', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: now, updatedAt: now, ); user = const AuthUser( uid: 'alice', email: 'alice@example.com', ); }); testWidgets('ambiguous AppBar Import icon is removed and USE EXISTING ROUTE action is visible', (tester) async { await tester.binding.setSurfaceSize(const Size(400, 900)); await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(RiderProfile(uid: 'alice', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: DateTime.now(), updatedAt: DateTime.now()))), ], child: const MaterialApp(debugShowCheckedModeBanner: false, home: CreateGroupRideScreen(), ), ), ); await tester.pumpAndSettle(); // Verify ambiguous AppBar icon is removed final appBarFinder = find.byType(AppBar); expect( find.descendant(of: appBarFinder, matching: find.byIcon(Icons.alt_route)), findsNothing, ); // Verify explicit form button "USE EXISTING ROUTE" is visible expect(find.text('USE EXISTING ROUTE'), findsOneWidget); }); testWidgets('blocks publish when Meetup or Destination is unresolved text', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final mockService = _MockGroupRideService(); await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(RiderProfile(uid: 'alice', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: DateTime.now(), updatedAt: DateTime.now()))), groupRideServiceProvider.overrideWithValue(mockService), ], child: const MaterialApp(debugShowCheckedModeBanner: false, home: CreateGroupRideScreen(), ), ), ); await tester.pumpAndSettle(); // Enter valid title final titleField = find.widgetWithText(TextFormField, 'Ride Title'); await tester.enterText(titleField, 'Weekend Cruise'); await tester.pumpAndSettle(); // Enter unresolved text into Meetup field final startSearchField = find.widgetWithText( AddressAutocompleteField, 'Meeting Place Address / Location *', ); final startInput = find.descendant( of: startSearchField, matching: find.byType(TextFormField), ); await tester.enterText(startInput, 'Unresolved Meetup Spot'); await tester.pumpAndSettle(); // Do not select resolved meetup from suggestions, just tap PUBLISH await tester.tap(find.text('PUBLISH')); await tester.pumpAndSettle(); // Validation error shown expect( find.textContaining('Please select a valid address from the suggestions'), findsOneWidget, ); expect(mockService.createdRide, isNull); }); testWidgets('calculates route and displays preview when Start and Destination resolved', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final fake = FakeRouteProvider( customSearchResults: [ const PlaceSearchResult( providerId: 'meetup_1', primaryText: 'Dahlonega Chevron', secondaryText: 'Dahlonega, GA', latitude: 34.53, longitude: -83.98, ), const PlaceSearchResult( providerId: 'dest_1', primaryText: 'Neels Gap', secondaryText: 'Blairsville, GA', latitude: 34.88, longitude: -83.92, ), ], ); await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(RiderProfile(uid: 'alice', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: DateTime.now(), updatedAt: DateTime.now()))), activeRouteCalculationProvider.overrideWithValue(fake), ], child: const MaterialApp(debugShowCheckedModeBanner: false, home: CreateGroupRideScreen(), ), ), ); await tester.pumpAndSettle(); // Fill Start address final startSearchField = find.widgetWithText( AddressAutocompleteField, 'Meeting Place Address / Location *', ); final startInput = find.descendant( of: startSearchField, matching: find.byType(TextFormField), ); await tester.enterText(startInput, 'Dahlonega'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Dahlonega Chevron').last); await tester.pumpAndSettle(); // Fill Destination address final destSearchField = find.widgetWithText( AddressAutocompleteField, 'Destination Address / Location *', ); final destInput = find.descendant( of: destSearchField, matching: find.byType(TextFormField), ); await tester.enterText(destInput, 'Neels'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Neels Gap').last); await tester.pumpAndSettle(); // Verify route preview section rendered with distance expect(find.text('ROUTE PREVIEW'), findsOneWidget); expect(find.textContaining('Road Est.'), findsOneWidget); expect(find.text('Powered by Valhalla • Stadia Maps'), findsOneWidget); }); testWidgets('allows adding, editing, and removing intermediate stops', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final fake = FakeRouteProvider( customSearchResults: [ const PlaceSearchResult( providerId: 'stop_p1', primaryText: 'Turner Corner Gas', secondaryText: 'Cleveland, GA', latitude: 34.68, longitude: -83.89, ), ], ); await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(RiderProfile(uid: 'alice', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: DateTime.now(), updatedAt: DateTime.now()))), activeRouteCalculationProvider.overrideWithValue(fake), ], child: const MaterialApp(debugShowCheckedModeBanner: false, home: CreateGroupRideScreen(), ), ), ); await tester.pumpAndSettle(); // Tap ADD STOP await tester.tap(find.text('ADD STOP')); await tester.pumpAndSettle(); expect(find.text('Add Intermediate Stop'), findsOneWidget); // Search stop address final dialogSearch = find.descendant( of: find.byType(AlertDialog), matching: find.byType(AddressAutocompleteField), ); final dialogSearchInput = find.descendant( of: dialogSearch, matching: find.byType(TextFormField), ); await tester.enterText(dialogSearchInput, 'Turner'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Turner Corner Gas')); await tester.pumpAndSettle(); // Add Stop await tester.tap(find.text('ADD STOP').last); await tester.pumpAndSettle(); // Verify stop added in intermediate list expect(find.text('Turner Corner Gas'), findsOneWidget); expect(find.textContaining('Intermediate Stops (2/25 max)'), findsOneWidget); // Delete stop await tester.tap(find.byIcon(Icons.delete_outline)); await tester.pumpAndSettle(); expect(find.text('Turner Corner Gas'), findsNothing); expect(find.textContaining('Intermediate Stops (1/25 max)'), findsOneWidget); }); testWidgets('USE EXISTING ROUTE loads saved route stops and recalculates route', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final savedRoute = MotorSignalRoute( id: 'route_saved_1', title: 'Georgia Mountain Loop', description: 'Twisty scenic mountain ride', ownerUid: 'alice', ownerHandle: 'alice_apex', visibility: RouteVisibility.public, straightLineDistanceMeters: 55000, stops: const [ RouteStop( id: 's_start', displayName: 'Suches Community Center', latitude: 34.689, longitude: -84.025, order: 0, stopType: RouteStopType.origin, ), RouteStop( id: 's_mid', displayName: 'Vogel State Park', latitude: 34.764, longitude: -83.924, order: 1, stopType: RouteStopType.waypoint, ), RouteStop( id: 's_end', displayName: 'Two Wheels of Suches', latitude: 34.693, longitude: -84.023, order: 2, stopType: RouteStopType.destination, ), ], createdAt: DateTime.now(), updatedAt: DateTime.now(), ); final fake = FakeRouteProvider(); await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(RiderProfile(uid: 'alice', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: DateTime.now(), updatedAt: DateTime.now()))), userRoutesProvider('alice').overrideWith((ref) => Stream.value([savedRoute])), activeRouteCalculationProvider.overrideWithValue(fake), ], child: const MaterialApp(debugShowCheckedModeBanner: false, home: CreateGroupRideScreen(), ), ), ); await tester.pumpAndSettle(); // Tap USE EXISTING ROUTE await tester.tap(find.text('USE EXISTING ROUTE')); await tester.pumpAndSettle(); // Verify bottom sheet appears expect(find.text('Georgia Mountain Loop'), findsOneWidget); // Select route await tester.tap(find.text('Georgia Mountain Loop')); await tester.pumpAndSettle(); // Verify fields populated expect(find.text('Georgia Mountain Loop'), findsWidgets); expect(find.text('Suches Community Center'), findsWidgets); expect(find.text('Vogel State Park'), findsOneWidget); expect(find.text('Two Wheels of Suches'), findsWidgets); expect(find.text('Route: Georgia Mountain Loop'), findsOneWidget); // Verify route preview recalculated expect(find.text('ROUTE PREVIEW'), findsOneWidget); }); testWidgets('submits and publishes GroupRide with valid resolved locations and stops', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final mockService = _MockGroupRideService(); final fake = FakeRouteProvider( customSearchResults: [ const PlaceSearchResult( providerId: 'meetup_1', primaryText: 'Dahlonega Chevron', secondaryText: 'Dahlonega, GA', latitude: 34.53, longitude: -83.98, ), const PlaceSearchResult( providerId: 'dest_1', primaryText: 'Neels Gap', secondaryText: 'Blairsville, GA', latitude: 34.88, longitude: -83.92, ), ], ); await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(RiderProfile(uid: 'alice', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: DateTime.now(), updatedAt: DateTime.now()))), activeRouteCalculationProvider.overrideWithValue(fake), groupRideServiceProvider.overrideWithValue(mockService), ], child: const MaterialApp(debugShowCheckedModeBanner: false, home: CreateGroupRideScreen(), ), ), ); await tester.pumpAndSettle(); // Fill title final titleField = find.widgetWithText(TextFormField, 'Ride Title'); await tester.enterText(titleField, 'Blood Mountain Fast Run'); // Meetup final startSearchField = find.widgetWithText( AddressAutocompleteField, 'Meeting Place Address / Location *', ); final startInput = find.descendant( of: startSearchField, matching: find.byType(TextFormField), ); await tester.enterText(startInput, 'Dahlonega'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Dahlonega Chevron').last); await tester.pumpAndSettle(); // Destination final destSearchField = find.widgetWithText( AddressAutocompleteField, 'Destination Address / Location *', ); final destInput = find.descendant( of: destSearchField, matching: find.byType(TextFormField), ); await tester.enterText(destInput, 'Neels'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Neels Gap').last); await tester.pumpAndSettle(); // Tap PUBLISH await tester.tap(find.text('PUBLISH')); await tester.pumpAndSettle(); // Verify ride created with correct coordinates expect(mockService.createdRide, isNotNull); expect(mockService.createdRide!.title, 'Blood Mountain Fast Run'); expect(mockService.createdRide!.meetingPoint.displayName, 'Dahlonega Chevron'); expect(mockService.createdRide!.meetingPoint.latitude, 34.53); expect(mockService.createdRide!.destination!.displayName, 'Neels Gap'); expect(mockService.createdRide!.destination!.latitude, 34.88); }); }); group('ROUTE-P2 Final Pre-Commit Forensic Gate', () { testWidgets('VERIFY EXACT ROUTE ORDER: Start + Destination', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final mockService = _MockGroupRideService(); final fakeRouteProvider = FakeRouteProvider( customSearchResults: [ const PlaceSearchResult(providerId: 'meetup_1', primaryText: 'Dahlonega Chevron', secondaryText: 'Dahlonega, GA', latitude: 34.53, longitude: -83.98), const PlaceSearchResult(providerId: 'stop_2', primaryText: 'Stop 2 Place', secondaryText: 'Stop 2 City', latitude: 34.70, longitude: -83.90), const PlaceSearchResult(providerId: 'dest_1', primaryText: 'Neels Gap', secondaryText: 'Blood Mountain', latitude: 34.88, longitude: -83.99), ], ); await tester.pumpWidget( ProviderScope( overrides: [ groupRideServiceProvider.overrideWithValue(mockService), myRiderProfileProvider.overrideWith((_) => Stream.value(RiderProfile( uid: 'uid_123', handle: 'testrider', displayName: 'Test Rider', createdAt: DateTime.now(), updatedAt: DateTime.now(), ))), activeRouteCalculationProvider.overrideWithValue(fakeRouteProvider), ], child: const MaterialApp( debugShowCheckedModeBanner: false, home: Material( child: CreateGroupRideScreen(), ), ), ), ); await tester.pumpAndSettle(); // Meetup final searchField = find.widgetWithText(AddressAutocompleteField, 'Meeting Place Address / Location *'); final input = find.descendant(of: searchField, matching: find.byType(TextFormField)); await tester.enterText(input, 'Start'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Dahlonega Chevron').last); await tester.pumpAndSettle(); // Destination (already visible, hasDestination=true by default) final destSearchField = find.widgetWithText(AddressAutocompleteField, 'Destination Address / Location *'); final destInput = find.descendant(of: destSearchField, matching: find.byType(TextFormField)); await tester.enterText(destInput, 'Neels'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Neels Gap').last); await tester.pumpAndSettle(); expect(fakeRouteProvider.lastCapturedStops, isNotNull); final stops = fakeRouteProvider.lastCapturedStops!; expect(stops.length, 2); expect(stops[0].stopType, RouteStopType.origin); expect(stops[0].displayName, 'Dahlonega Chevron'); expect(stops[1].stopType, RouteStopType.destination); expect(stops[1].displayName, 'Neels Gap'); }); testWidgets('VERIFY EXACT ROUTE ORDER: Start + Stop1 + Destination', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final mockService = _MockGroupRideService(); final fakeRouteProvider = FakeRouteProvider( customSearchResults: [ const PlaceSearchResult(providerId: 'meetup_1', primaryText: 'Dahlonega Chevron', secondaryText: 'Dahlonega, GA', latitude: 34.53, longitude: -83.98), const PlaceSearchResult(providerId: 'stop_2', primaryText: 'Stop 2 Place', secondaryText: 'Stop 2 City', latitude: 34.70, longitude: -83.90), const PlaceSearchResult(providerId: 'dest_1', primaryText: 'Neels Gap', secondaryText: 'Blood Mountain', latitude: 34.88, longitude: -83.99), ], ); await tester.pumpWidget( ProviderScope( overrides: [ groupRideServiceProvider.overrideWithValue(mockService), myRiderProfileProvider.overrideWith((_) => Stream.value(RiderProfile( uid: 'uid_123', handle: 'testrider', displayName: 'Test Rider', createdAt: DateTime.now(), updatedAt: DateTime.now(), ))), activeRouteCalculationProvider.overrideWithValue(fakeRouteProvider), ], child: const MaterialApp( debugShowCheckedModeBanner: false, home: Material( child: CreateGroupRideScreen(), ), ), ), ); await tester.pumpAndSettle(); // Meetup final searchField = find.widgetWithText(AddressAutocompleteField, 'Meeting Place Address / Location *'); final input = find.descendant(of: searchField, matching: find.byType(TextFormField)); await tester.enterText(input, 'Start'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Dahlonega Chevron').last); await tester.pumpAndSettle(); // Stop 1 await tester.dragUntilVisible(find.text('ADD STOP').first, find.byType(Scrollable).first, const Offset(0, -100)); await tester.pumpAndSettle(); await tester.tap(find.text('ADD STOP').first); await tester.pumpAndSettle(); final stopSearchField = find.widgetWithText(AddressAutocompleteField, 'Stop Address / Location *'); final stopInput = find.descendant(of: stopSearchField, matching: find.byType(TextFormField)); await tester.enterText(stopInput, 'Stop1'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Dahlonega Chevron').last); // Fake returns this for everything await tester.pumpAndSettle(); await tester.tap(find.text('ADD STOP').last); // Add button in dialog await tester.pumpAndSettle(); // Destination final destSearchField = find.widgetWithText(AddressAutocompleteField, 'Destination Address / Location *'); final destInput = find.descendant(of: destSearchField, matching: find.byType(TextFormField)); await tester.enterText(destInput, 'Neels'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Neels Gap').last); await tester.pumpAndSettle(); expect(fakeRouteProvider.lastCapturedStops, isNotNull); final stops = fakeRouteProvider.lastCapturedStops!; expect(stops.length, 3); expect(stops[0].stopType, RouteStopType.origin); expect(stops[1].stopType, RouteStopType.waypoint); expect(stops[2].stopType, RouteStopType.destination); }); testWidgets('VERIFY EXACT ROUTE ORDER: Start + Stop1 + Stop2 + Destination', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final mockService = _MockGroupRideService(); final fakeRouteProvider = FakeRouteProvider( customSearchResults: [ const PlaceSearchResult(providerId: 'meetup_1', primaryText: 'Dahlonega Chevron', secondaryText: 'Dahlonega, GA', latitude: 34.53, longitude: -83.98), const PlaceSearchResult(providerId: 'stop_2', primaryText: 'Stop 2 Place', secondaryText: 'Stop 2 City', latitude: 34.70, longitude: -83.90), const PlaceSearchResult(providerId: 'dest_1', primaryText: 'Neels Gap', secondaryText: 'Blood Mountain', latitude: 34.88, longitude: -83.99), ], ); await tester.pumpWidget( ProviderScope( overrides: [ groupRideServiceProvider.overrideWithValue(mockService), myRiderProfileProvider.overrideWith((_) => Stream.value(RiderProfile( uid: 'uid_123', handle: 'testrider', displayName: 'Test Rider', createdAt: DateTime.now(), updatedAt: DateTime.now(), ))), activeRouteCalculationProvider.overrideWithValue(fakeRouteProvider), ], child: const MaterialApp( debugShowCheckedModeBanner: false, home: Material( child: CreateGroupRideScreen(), ), ), ), ); await tester.pumpAndSettle(); // Meetup final searchField = find.widgetWithText(AddressAutocompleteField, 'Meeting Place Address / Location *'); final input = find.descendant(of: searchField, matching: find.byType(TextFormField)); await tester.enterText(input, 'Start'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Dahlonega Chevron').last); await tester.pumpAndSettle(); // Stop 1 await tester.dragUntilVisible(find.text('ADD STOP').first, find.byType(Scrollable).first, const Offset(0, -100)); await tester.pumpAndSettle(); await tester.tap(find.text('ADD STOP').first); await tester.pumpAndSettle(); final stopSearchField1 = find.widgetWithText(AddressAutocompleteField, 'Stop Address / Location *'); final stopInput1 = find.descendant(of: stopSearchField1, matching: find.byType(TextFormField)); await tester.enterText(stopInput1, 'Stop1'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Dahlonega Chevron').last); await tester.pumpAndSettle(); await tester.tap(find.text('ADD STOP').last); await tester.pumpAndSettle(); // Stop 2 await tester.dragUntilVisible(find.text('ADD STOP').first, find.byType(Scrollable).first, const Offset(0, -100)); await tester.pumpAndSettle(); await tester.tap(find.text('ADD STOP').first); await tester.pumpAndSettle(); final stopSearchFields = find.widgetWithText(AddressAutocompleteField, 'Stop Address / Location *'); final stopInput2 = find.descendant(of: stopSearchFields.last, matching: find.byType(TextFormField)); await tester.enterText(stopInput2, 'Stop2'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Stop 2 Place').last); await tester.pumpAndSettle(); await tester.tap(find.text('ADD STOP').last); await tester.pumpAndSettle(); // Destination final destSearchField = find.widgetWithText(AddressAutocompleteField, 'Destination Address / Location *'); final destInput = find.descendant(of: destSearchField, matching: find.byType(TextFormField)); await tester.enterText(destInput, 'Neels'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('Neels Gap').last); await tester.pumpAndSettle(); expect(fakeRouteProvider.lastCapturedStops, isNotNull); final stops = fakeRouteProvider.lastCapturedStops!; expect(stops.length, 4); expect(stops[0].stopType, RouteStopType.origin); expect(stops[1].stopType, RouteStopType.waypoint); expect(stops[2].stopType, RouteStopType.waypoint); expect(stops[3].stopType, RouteStopType.destination); }); }); group('ROUTE-P2 REAL RESPONSIVE TESTS', () { testWidgets('Pixel-like portrait (412x915)', (tester) async { await tester.binding.setSurfaceSize(const Size(412, 915)); final fakeRouteProvider = FakeRouteProvider( customSearchResults: [ const PlaceSearchResult(providerId: 'meetup_1', primaryText: 'Result 1', secondaryText: 'City 1', latitude: 34.1, longitude: -83.1), ], ); final now = DateTime.now(); final profile = RiderProfile(uid: 'alice', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: now, updatedAt: now); final user = const AuthUser(uid: 'alice', email: 'alice@example.com'); final container = ProviderContainer( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(profile)), activeRouteCalculationProvider.overrideWithValue(fakeRouteProvider), ], ); addTearDown(() { tester.binding.setSurfaceSize(null); container.dispose(); }); await tester.pumpWidget( UncontrolledProviderScope( container: container, child: MaterialApp( home: const CreateGroupRideScreen(), ), ), ); await tester.pumpAndSettle(); final searchFields = find.widgetWithText(AddressAutocompleteField, 'Meeting Place Address / Location *'); final meetupInput = find.descendant(of: searchFields.first, matching: find.byType(TextFormField)); await tester.enterText(meetupInput, 'Start'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); FocusManager.instance.primaryFocus?.unfocus(); await tester.pumpAndSettle(); for (int i = 0; i < 20; i++) { if (tester.any(find.text('ADD STOP').first)) { final dy = tester.getCenter(find.text('ADD STOP').first).dy; if (dy > 100 && dy < 300) break; } await tester.drag(find.byType(Scrollable).first, const Offset(0, -100)); await tester.pumpAndSettle(); } await tester.tap(find.text('ADD STOP').first); await tester.pumpAndSettle(); final stopFields = find.widgetWithText(AddressAutocompleteField, 'Stop Address / Location *'); final stopInput = find.descendant(of: stopFields.last, matching: find.byType(TextFormField)); await tester.enterText(stopInput, 'Stop1'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('CANCEL')); await tester.pumpAndSettle(); for (int i = 0; i < 20; i++) { final destSearchField = find.widgetWithText(AddressAutocompleteField, 'Destination Address / Location *'); if (tester.any(destSearchField)) { final dy = tester.getCenter(destSearchField).dy; if (dy > 100 && dy < 300) break; } await tester.drag(find.byType(Scrollable).first, const Offset(0, -100)); await tester.pumpAndSettle(); } final destSearchField = find.widgetWithText(AddressAutocompleteField, 'Destination Address / Location *'); final destInput = find.descendant(of: destSearchField, matching: find.byType(TextFormField)); await tester.enterText(destInput, 'Dest'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); testWidgets('Short landscape (915x412)', (tester) async { await tester.binding.setSurfaceSize(const Size(915, 412)); final fakeRouteProvider = FakeRouteProvider( customSearchResults: [ const PlaceSearchResult(providerId: 'meetup_1', primaryText: 'Result 1', secondaryText: 'City 1', latitude: 34.1, longitude: -83.1), ], ); final now = DateTime.now(); final profile = RiderProfile(uid: 'alice', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: now, updatedAt: now); final user = const AuthUser(uid: 'alice', email: 'alice@example.com'); final container = ProviderContainer( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(profile)), activeRouteCalculationProvider.overrideWithValue(fakeRouteProvider), ], ); addTearDown(() { tester.binding.setSurfaceSize(null); container.dispose(); }); await tester.pumpWidget( UncontrolledProviderScope( container: container, child: MaterialApp( home: const CreateGroupRideScreen(), ), ), ); await tester.pumpAndSettle(); final searchFields = find.widgetWithText(AddressAutocompleteField, 'Meeting Place Address / Location *'); final meetupInput = find.descendant(of: searchFields.first, matching: find.byType(TextFormField)); await tester.enterText(meetupInput, 'Start'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); FocusManager.instance.primaryFocus?.unfocus(); await tester.pumpAndSettle(); for (int i = 0; i < 20; i++) { if (tester.any(find.text('ADD STOP').first)) { final dy = tester.getCenter(find.text('ADD STOP').first).dy; if (dy > 100 && dy < 300) break; } await tester.drag(find.byType(Scrollable).first, const Offset(0, -100)); await tester.pumpAndSettle(); } await tester.tap(find.text('ADD STOP').first); await tester.pumpAndSettle(); final stopFields = find.widgetWithText(AddressAutocompleteField, 'Stop Address / Location *'); final stopInput = find.descendant(of: stopFields.last, matching: find.byType(TextFormField)); await tester.enterText(stopInput, 'Stop1'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); await tester.tap(find.text('CANCEL')); await tester.pumpAndSettle(); for (int i = 0; i < 20; i++) { final destSearchField = find.widgetWithText(AddressAutocompleteField, 'Destination Address / Location *'); if (tester.any(destSearchField)) { final dy = tester.getCenter(destSearchField).dy; if (dy > 100 && dy < 300) break; } await tester.drag(find.byType(Scrollable).first, const Offset(0, -100)); await tester.pumpAndSettle(); } final destSearchField = find.widgetWithText(AddressAutocompleteField, 'Destination Address / Location *'); final destInput = find.descendant(of: destSearchField, matching: find.byType(TextFormField)); await tester.enterText(destInput, 'Dest'); await tester.pump(const Duration(milliseconds: 400)); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); testWidgets('ROUTE RACE - In-flight calculation is discarded if sequence advances', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); Completer? calc1Completer; final fakeRouteProvider = FakeRouteProvider( customSearchResults: [ const PlaceSearchResult(providerId: 'meetup_1', primaryText: 'Start', secondaryText: 'City 1', latitude: 34.1, longitude: -83.1), const PlaceSearchResult(providerId: 'dest_1', primaryText: 'Dest1', secondaryText: 'City 2', latitude: 34.2, longitude: -83.2), ], onCalculate: (count) async { print('ON CALCULATE CALLED WITH COUNT: '); if (count == 0) { calc1Completer = Completer(); await calc1Completer!.future; } }, ); final container = ProviderContainer( overrides: [ activeRouteCalculationProvider.overrideWithValue(fakeRouteProvider), currentUserProvider.overrideWithValue(const AuthUser(uid: 'alice', email: 'alice@example.com')), ], ); await tester.pumpWidget( UncontrolledProviderScope( container: container, child: const MaterialApp(home: CreateGroupRideScreen()), ), ); await tester.pumpAndSettle(); // Step 1: Add Meetup final meetupField = find.widgetWithText(AddressAutocompleteField, 'Meeting Place Address / Location *'); await tester.enterText(find.descendant(of: meetupField, matching: find.byType(TextFormField)), 'Start'); await tester.pumpAndSettle(); await tester.tap(find.text('Start').last); await tester.pumpAndSettle(); // Step 2: Add Destination final destField = find.widgetWithText(AddressAutocompleteField, 'Destination Address / Location *'); await tester.enterText(find.descendant(of: destField, matching: find.byType(TextFormField)), 'Dest1'); await tester.pumpAndSettle(); // Tap Dest1 -> triggers Calculation 0 (which stalls because count == 0) await tester.tap(find.text('Dest1').last); await tester.pump(); // Start calc // Step 3: While Calc 0 is stalled, clear the destination! // This triggers sequence increment and an early return. await tester.enterText(find.descendant(of: destField, matching: find.byType(TextFormField)), ''); // Tap the 'clear' icon if there is one, or just unfocus FocusManager.instance.primaryFocus?.unfocus(); await tester.pumpAndSettle(); // Step 4: Now finish Calc 0! calc1Completer?.complete(); await tester.pumpAndSettle(); // Verification: The stale Calc 0 should NOT be shown! // The UI should show no distance label. expect(find.textContaining('miles'), findsNothing); expect(find.textContaining('km'), findsNothing); }); }); group('CreateGroupRideScreen Club Ride Creation Tests', () { late RiderProfile profile; late AuthUser user; late Club testClub; late ClubMember adminMember; late ClubMember regularMember; setUp(() { final now = DateTime.now(); profile = RiderProfile( uid: 'alice', handle: 'alice_apex', displayName: 'Alice Apex', createdAt: now, updatedAt: now, ); user = const AuthUser( uid: 'alice', email: 'alice@example.com', ); testClub = Club( id: 'club_alpha', name: 'Alpha Moto Club', slug: 'alpha-moto', description: 'Elite riders club', ownerUid: 'alice', createdAt: now, updatedAt: now, ); adminMember = ClubMember( clubId: 'club_alpha', uid: 'alice', role: ClubRole.admin, status: ClubMemberStatus.active, joinedAt: now, updatedAt: now, ); regularMember = ClubMember( clubId: 'club_alpha', uid: 'alice', role: ClubRole.member, status: ClubMemberStatus.active, joinedAt: now, updatedAt: now, ); }); testWidgets('Personal ride creation flow: no club header and creates with null clubId', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final mockService = _MockGroupRideService(); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(profile)), groupRideServiceProvider.overrideWithValue(mockService), ], child: const MaterialApp( debugShowCheckedModeBanner: false, home: CreateGroupRideScreen(), ), ), ); await tester.pumpAndSettle(); expect(find.text('CREATE GROUP RIDE'), findsOneWidget); expect(find.text('CLUB RIDE'), findsNothing); expect(find.textContaining('Planning for'), findsNothing); expect(find.text('PUBLISH GROUP RIDE'), findsOneWidget); }); testWidgets('Club ride creation: active admin sees club header and creates with clubId', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final mockService = _MockGroupRideService(); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(profile)), groupRideServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_alpha').overrideWith((ref) => Stream.value(testClub)), currentMemberProvider('club_alpha').overrideWith((ref) => Stream.value(adminMember)), ], child: const MaterialApp( debugShowCheckedModeBanner: false, home: CreateGroupRideScreen(clubId: 'club_alpha'), ), ), ); await tester.pumpAndSettle(); expect(find.text('CREATE CLUB RIDE'), findsOneWidget); expect(find.text('CLUB RIDE'), findsOneWidget); expect(find.text('Planning for Alpha Moto Club'), findsOneWidget); expect(find.text('PUBLISH CLUB RIDE'), findsOneWidget); }); testWidgets('Club ride creation: ordinary member is blocked with permission error', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final mockService = _MockGroupRideService(); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(profile)), groupRideServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_alpha').overrideWith((ref) => Stream.value(testClub)), currentMemberProvider('club_alpha').overrideWith((ref) => Stream.value(regularMember)), ], child: const MaterialApp( debugShowCheckedModeBanner: false, home: CreateGroupRideScreen(clubId: 'club_alpha'), ), ), ); await tester.pumpAndSettle(); expect(find.text('CREATE CLUB RIDE'), findsOneWidget); expect(find.text('Club Affiliation Error'), findsOneWidget); expect(find.text('Only club owners and administrators can create club rides.'), findsOneWidget); final publishBtn = tester.widget(find.widgetWithText(ElevatedButton, 'PUBLISH CLUB RIDE')); expect(publishBtn.onPressed, isNull); }); testWidgets('Club ride creation: archived club is blocked with error', (tester) async { await tester.binding.setSurfaceSize(const Size(800, 2400)); addTearDown(() => tester.binding.setSurfaceSize(null)); final mockService = _MockGroupRideService(); final archivedClub = Club( id: 'club_alpha', name: 'Archived Moto Club', slug: 'archived-moto', ownerUid: 'alice', archivedAt: DateTime.now(), createdAt: DateTime.now(), updatedAt: DateTime.now(), ); await tester.pumpWidget( ProviderScope( overrides: [ currentUserProvider.overrideWithValue(user), myRiderProfileProvider.overrideWith((ref) => Stream.value(profile)), groupRideServiceProvider.overrideWithValue(mockService), clubDetailProvider('club_alpha').overrideWith((ref) => Stream.value(archivedClub)), currentMemberProvider('club_alpha').overrideWith((ref) => Stream.value(adminMember)), ], child: const MaterialApp( debugShowCheckedModeBanner: false, home: CreateGroupRideScreen(clubId: 'club_alpha'), ), ), ); await tester.pumpAndSettle(); expect(find.text('Club Affiliation Error'), findsOneWidget); expect(find.text('This club has been archived and cannot organize rides.'), findsOneWidget); final publishBtn = tester.widget(find.widgetWithText(ElevatedButton, 'PUBLISH CLUB RIDE')); expect(publishBtn.onPressed, isNull); }); }); }