542 lines
18 KiB
Dart
542 lines
18 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:motorcycle_dash/core/theme/theme.dart';
|
|
import 'package:motorcycle_dash/features/group_rides/group_rides_hub_screen.dart';
|
|
import 'package:motorcycle_dash/features/group_rides/create_group_ride_screen.dart';
|
|
import 'package:motorcycle_dash/features/group_rides/group_ride_detail_screen.dart';
|
|
import 'package:motorcycle_dash/features/group_rides/live_group_ride_screen.dart';
|
|
import 'package:motorcycle_dash/features/social/rider_profile_screen.dart';
|
|
import 'package:motorcycle_dash/features/social/friends_screen.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_providers.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_service.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_state.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_providers.dart';
|
|
import 'package:motorcycle_dash/services/group_rides/live_broadcast_controller.dart';
|
|
import 'package:motorcycle_dash/services/social/rider_profile_models.dart';
|
|
import 'package:motorcycle_dash/services/social/social_providers.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:motorcycle_dash/features/records/records_service.dart';
|
|
|
|
class _FakeAuth implements AuthService {
|
|
@override Future<void> ensureBackendIdentity() async {}
|
|
|
|
final _states = StreamController<AuthState>.broadcast();
|
|
AuthUser? user;
|
|
|
|
_FakeAuth({this.user});
|
|
|
|
@override
|
|
Stream<AuthState> get authStateChanges => _states.stream;
|
|
|
|
@override
|
|
AuthState get currentState => user == null
|
|
? const AuthState.unauthenticated(isFirebaseConfigured: true)
|
|
: (user!.isAnonymous
|
|
? AuthState.anonymous(user!, isFirebaseConfigured: true)
|
|
: AuthState.authenticated(user!, isFirebaseConfigured: true));
|
|
|
|
@override
|
|
AuthUser? get currentUser => user;
|
|
|
|
@override
|
|
bool get isConfigured => true;
|
|
|
|
@override
|
|
bool get isEmailVerified => true;
|
|
|
|
@override
|
|
Future<void> deleteCurrentUser() async {}
|
|
|
|
@override
|
|
void dispose() => _states.close();
|
|
|
|
@override
|
|
Future<String?> getIdToken([bool forceRefresh = false]) async => 'token';
|
|
|
|
@override
|
|
Future<void> reauthenticate({String? password}) async {}
|
|
|
|
@override
|
|
Future<void> reloadCurrentUser() async {}
|
|
|
|
@override
|
|
Future<void> sendEmailVerification() async {}
|
|
|
|
@override
|
|
Future<void> sendPasswordResetEmail(String email) async {}
|
|
|
|
@override
|
|
Future<void> signInAnonymously() async {}
|
|
|
|
@override
|
|
Future<void> signInWithEmailAndPassword(String email, String password) async {}
|
|
|
|
@override
|
|
Future<void> createUserWithEmailAndPassword(String email, String password,
|
|
{String? displayName}) async {}
|
|
|
|
@override
|
|
Future<void> signInWithGoogle() async {}
|
|
|
|
@override
|
|
Future<void> signOut() async {}
|
|
}
|
|
|
|
class _FakeLiveBroadcastController extends StateNotifier<LiveBroadcastState>
|
|
implements LiveBroadcastController {
|
|
_FakeLiveBroadcastController(super.initialState);
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
|
|
@override
|
|
Future<void> setOptIn(bool optIn) async {}
|
|
|
|
@override
|
|
Future<void> setStatus(LivePresenceStatus status) async {}
|
|
|
|
@override
|
|
Future<void> triggerRegroup() async {}
|
|
|
|
@override
|
|
Future<void> clearRegroup() async {}
|
|
|
|
@override
|
|
void updateAppLifecycle(AppLifecycleState state) {}
|
|
}
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
late SharedPreferences prefs;
|
|
|
|
setUp(() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
prefs = await SharedPreferences.getInstance();
|
|
});
|
|
|
|
const testUser = AuthUser(
|
|
uid: 'uid_lead',
|
|
email: 'rider@motorsignal.app',
|
|
displayName: 'Canyon Lead',
|
|
isAnonymous: false,
|
|
);
|
|
|
|
final testProfile = RiderProfile(
|
|
uid: 'uid_lead',
|
|
handle: 'canyon_lead',
|
|
displayName: 'Canyon Lead',
|
|
bio: 'Twisty roads and track days',
|
|
region: 'Blue Ridge, NC',
|
|
avatarPreset: 'preset_neon_cyan',
|
|
privacySettings: const ProfilePrivacySettings(
|
|
isSearchableByHandle: true,
|
|
isProfilePublic: true,
|
|
allowFriendRequests: true,
|
|
),
|
|
createdAt: DateTime(2026, 1, 1),
|
|
updatedAt: DateTime(2026, 1, 1),
|
|
);
|
|
|
|
final now = DateTime(2026, 9, 1, 12, 0);
|
|
|
|
final testRide = GroupRide(
|
|
id: 'ride_101',
|
|
organizerUid: 'uid_lead',
|
|
organizerHandle: 'canyon_lead',
|
|
organizerDisplayName: 'Canyon Lead',
|
|
title: 'Blue Ridge Twisties Run',
|
|
description: 'Scenic group ride through Blue Ridge twisties.',
|
|
scheduledStartAt: DateTime(2026, 10, 15, 9, 0),
|
|
departureAt: DateTime(2026, 10, 15, 9, 30),
|
|
status: GroupRideStatus.underway,
|
|
joinPolicy: GroupRideJoinPolicy.open,
|
|
visibility: GroupRideVisibility.public,
|
|
meetingPoint: const GroupRideLocation(
|
|
latitude: 35.5951,
|
|
longitude: -82.5515,
|
|
displayName: 'Asheville Meetup',
|
|
locationType: GroupRideLocationType.meetup,
|
|
),
|
|
destination: const GroupRideLocation(
|
|
latitude: 35.7951,
|
|
longitude: -82.2515,
|
|
displayName: 'Mount Mitchell Peak',
|
|
locationType: GroupRideLocationType.destination,
|
|
),
|
|
participantCount: 3,
|
|
stops: const [
|
|
GroupRideLocation(
|
|
latitude: 35.65,
|
|
longitude: -82.40,
|
|
displayName: 'Scenic Overlook Stop',
|
|
locationType: GroupRideLocationType.scenic,
|
|
),
|
|
],
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
);
|
|
|
|
final List<GroupRideParticipant> testParticipants = [
|
|
GroupRideParticipant(
|
|
userId: 'uid_lead',
|
|
riderHandle: 'canyon_lead',
|
|
riderDisplayName: 'Canyon Lead',
|
|
role: GroupRideRole.lead,
|
|
status: ParticipantStatus.accepted,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
),
|
|
GroupRideParticipant(
|
|
userId: 'uid_sweep',
|
|
riderHandle: 'tail_gunner',
|
|
riderDisplayName: 'Tail Gunner',
|
|
role: GroupRideRole.sweep,
|
|
status: ParticipantStatus.accepted,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
),
|
|
GroupRideParticipant(
|
|
userId: 'uid_rider3',
|
|
riderHandle: 'moto_enthusiast',
|
|
riderDisplayName: 'Moto Enthusiast',
|
|
role: GroupRideRole.participant,
|
|
status: ParticipantStatus.accepted,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
),
|
|
];
|
|
|
|
final List<LiveGroupPresence> testPresences = [
|
|
LiveGroupPresence(
|
|
userId: 'uid_lead',
|
|
groupRideId: 'ride_101',
|
|
latitude: 35.5951,
|
|
longitude: -82.5515,
|
|
heading: 45.0,
|
|
speedMph: 45.0,
|
|
accuracyMeters: 5.0,
|
|
status: LivePresenceStatus.riding,
|
|
sharingEnabled: true,
|
|
updatedAt: now,
|
|
expiresAt: now.add(const Duration(seconds: 120)),
|
|
),
|
|
LiveGroupPresence(
|
|
userId: 'uid_sweep',
|
|
groupRideId: 'ride_101',
|
|
latitude: 35.5900,
|
|
longitude: -82.5500,
|
|
heading: 40.0,
|
|
speedMph: 42.0,
|
|
accuracyMeters: 5.0,
|
|
status: LivePresenceStatus.riding,
|
|
sharingEnabled: true,
|
|
updatedAt: now.subtract(const Duration(seconds: 8)),
|
|
expiresAt: now.add(const Duration(seconds: 112)),
|
|
),
|
|
];
|
|
|
|
final List<FriendSummary> testFriends = [
|
|
FriendSummary(
|
|
uid: 'uid_sweep',
|
|
handle: 'tail_gunner',
|
|
displayName: 'Tail Gunner',
|
|
avatarPreset: 'preset_amber_flame',
|
|
friendedAt: DateTime(2026, 8, 1),
|
|
),
|
|
FriendSummary(
|
|
uid: 'uid_rider3',
|
|
handle: 'moto_enthusiast',
|
|
displayName: 'Moto Enthusiast',
|
|
avatarPreset: 'preset_electric_green',
|
|
friendedAt: DateTime(2026, 8, 10),
|
|
),
|
|
];
|
|
|
|
final List<FriendRequest> testIncomingRequests = [
|
|
FriendRequest(
|
|
requestId: 'req_1',
|
|
senderUid: 'uid_guest',
|
|
senderHandle: 'speed_demon',
|
|
senderDisplayName: 'Speed Demon',
|
|
senderAvatarPreset: 'preset_hyper_purple',
|
|
receiverUid: 'uid_lead',
|
|
sentAt: DateTime(2026, 9, 4),
|
|
),
|
|
];
|
|
|
|
final viewports = <String, Size>{
|
|
'Compact Phone (320x640)': const Size(320, 640),
|
|
'Standard Phone (400x800)': const Size(400, 800),
|
|
'Tall Phone (412x915)': const Size(412, 915),
|
|
'Small Landscape (640x320)': const Size(640, 320),
|
|
'Short Moto Mount Landscape (780x360)': const Size(780, 360),
|
|
'Standard Landscape (800x360)': const Size(800, 360),
|
|
'Wide Landscape (840x390)': const Size(840, 390),
|
|
'Medium Tablet Portrait (768x1024)': const Size(768, 1024),
|
|
'Expanded Tablet Landscape (1024x768)': const Size(1024, 768),
|
|
};
|
|
|
|
group('GroupRidesHubScreen Responsive Viewport Matrix', () {
|
|
for (final entry in viewports.entries) {
|
|
testWidgets('Renders cleanly without overflow on ${entry.key}',
|
|
(tester) async {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
final fakeAuth = _FakeAuth(user: testUser);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(fakeAuth),
|
|
currentUserProvider.overrideWithValue(testUser),
|
|
myRiderProfileProvider.overrideWith(
|
|
(ref) => Stream.value(testProfile),
|
|
),
|
|
upcomingGroupRidesProvider.overrideWith(
|
|
(ref) => Stream.value([testRide]),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const GroupRidesHubScreen(),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('GROUP RIDES'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|
|
});
|
|
|
|
group('CreateGroupRideScreen Responsive Matrix', () {
|
|
for (final entry in [
|
|
const MapEntry('Compact (400x800)', Size(400, 800)),
|
|
const MapEntry('Short Landscape (800x360)', Size(800, 360)),
|
|
const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)),
|
|
]) {
|
|
testWidgets('Renders cleanly on ${entry.key}', (tester) async {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
final fakeAuth = _FakeAuth(user: testUser);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(fakeAuth),
|
|
currentUserProvider.overrideWithValue(testUser),
|
|
myRiderProfileProvider.overrideWith(
|
|
(ref) => Stream.value(testProfile),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const CreateGroupRideScreen(),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('CREATE GROUP RIDE'), findsOneWidget);
|
|
expect(find.text('1. RIDE DETAILS'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|
|
});
|
|
|
|
group('GroupRideDetailScreen Responsive Matrix', () {
|
|
for (final entry in [
|
|
const MapEntry('Compact (400x800)', Size(400, 800)),
|
|
const MapEntry('Short Landscape (800x360)', Size(800, 360)),
|
|
const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)),
|
|
]) {
|
|
testWidgets('Renders 2-column on wide and details cleanly on ${entry.key}',
|
|
(tester) async {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
final fakeAuth = _FakeAuth(user: testUser);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(fakeAuth),
|
|
currentUserProvider.overrideWithValue(testUser),
|
|
groupRideStreamProvider('ride_101').overrideWith(
|
|
(ref) => Stream.value(testRide),
|
|
),
|
|
groupRideParticipantsProvider('ride_101').overrideWith(
|
|
(ref) => Stream.value(testParticipants),
|
|
),
|
|
groupRideLivePresenceProvider('ride_101').overrideWith(
|
|
(ref) => Stream.value(testPresences),
|
|
),
|
|
myRiderProfileProvider.overrideWith(
|
|
(ref) => Stream.value(testProfile),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const GroupRideDetailScreen(rideId: 'ride_101'),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Blue Ridge Twisties Run'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|
|
});
|
|
|
|
group('LiveGroupRideScreen Responsive Matrix', () {
|
|
for (final entry in [
|
|
const MapEntry('Compact (400x800)', Size(400, 800)),
|
|
const MapEntry('Short Landscape (800x360)', Size(800, 360)),
|
|
const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)),
|
|
]) {
|
|
testWidgets('Renders map, status bar, and controls on ${entry.key}',
|
|
(tester) async {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
final fakeAuth = _FakeAuth(user: testUser);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(fakeAuth),
|
|
currentUserProvider.overrideWithValue(testUser),
|
|
groupRideStreamProvider('ride_101').overrideWith(
|
|
(ref) => Stream.value(testRide),
|
|
),
|
|
groupRideParticipantsProvider('ride_101').overrideWith(
|
|
(ref) => Stream.value(testParticipants),
|
|
),
|
|
groupRideLivePresenceProvider('ride_101').overrideWith(
|
|
(ref) => Stream.value(testPresences),
|
|
),
|
|
liveBroadcastControllerProvider('ride_101').overrideWith(
|
|
(ref) => _FakeLiveBroadcastController(
|
|
const LiveBroadcastState(
|
|
isOptedIn: true,
|
|
isBroadcasting: true,
|
|
status: LivePresenceStatus.riding,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const LiveGroupRideScreen(rideId: 'ride_101'),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const ValueKey('live_ride_broadcast_switch')),
|
|
findsOneWidget);
|
|
expect(find.byKey(const ValueKey('live_ride_recenter_button')),
|
|
findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|
|
});
|
|
|
|
group('RiderProfileScreen & FriendsScreen Responsive Matrix', () {
|
|
for (final entry in [
|
|
const MapEntry('Compact (400x800)', Size(400, 800)),
|
|
const MapEntry('Wide (1024x768)', Size(1024, 768)),
|
|
]) {
|
|
testWidgets('RiderProfileScreen renders on ${entry.key}', (tester) async {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
final fakeAuth = _FakeAuth(user: testUser);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(fakeAuth),
|
|
currentUserProvider.overrideWithValue(testUser),
|
|
myRiderProfileProvider.overrideWith(
|
|
(ref) => Stream.value(testProfile),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const RiderProfileScreen(),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Rider Profile'), findsOneWidget);
|
|
expect(find.text('PRIVACY CONTROLS'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('FriendsScreen renders on ${entry.key}', (tester) async {
|
|
tester.view.physicalSize = entry.value;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
final fakeAuth = _FakeAuth(user: testUser);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(fakeAuth),
|
|
currentUserProvider.overrideWithValue(testUser),
|
|
myRiderProfileProvider.overrideWith(
|
|
(ref) => Stream.value(testProfile),
|
|
),
|
|
myFriendsProvider.overrideWith(
|
|
(ref) => Stream.value(testFriends),
|
|
),
|
|
myIncomingRequestsProvider.overrideWith(
|
|
(ref) => Stream.value(testIncomingRequests),
|
|
),
|
|
myOutgoingRequestsProvider.overrideWith(
|
|
(ref) => Stream.value(<FriendRequest>[]),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const FriendsScreen(),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Friends & Riders'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|
|
});
|
|
}
|