964 lines
38 KiB
Dart
964 lines
38 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 '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,
|
|
}) 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,
|
|
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);
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
});
|
|
}
|