MotorSignal-App/test/live_group_ride_screen_widg...

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);
}
});
}
});
}