MotorSignal-App/test/create_group_ride_screen_te...

1135 lines
45 KiB
Dart

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<GroupRide> 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<GroupRideLocation> stops = const [],
List<String> 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<void>? 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<void>();
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<ElevatedButton>(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<ElevatedButton>(find.widgetWithText(ElevatedButton, 'PUBLISH CLUB RIDE'));
expect(publishBtn.onPressed, isNull);
});
});
}