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