447 lines
14 KiB
Dart
447 lines
14 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:ui' as ui;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shared_preferences/shared_preferences.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/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/social/rider_profile_models.dart';
|
|
import 'package:motorcycle_dash/services/social/social_providers.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 {}
|
|
}
|
|
|
|
Future<void> _captureScreen(
|
|
WidgetTester tester,
|
|
GlobalKey repaintKey,
|
|
String filePath,
|
|
) async {
|
|
await tester.binding.runAsync(() async {
|
|
final boundary =
|
|
repaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
|
|
if (boundary == null) {
|
|
throw StateError('RepaintBoundary not found for capture');
|
|
}
|
|
final image = await boundary.toImage(pixelRatio: 2.0);
|
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
if (byteData == null) {
|
|
throw StateError('Failed to encode image to PNG');
|
|
}
|
|
final pngBytes = byteData.buffer.asUint8List();
|
|
final file = File(filePath);
|
|
await file.parent.create(recursive: true);
|
|
await file.writeAsBytes(pngBytes, flush: true);
|
|
});
|
|
}
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
late SharedPreferences prefs;
|
|
|
|
setUp(() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
prefs = await SharedPreferences.getInstance();
|
|
});
|
|
|
|
const ownerUser = AuthUser(
|
|
uid: 'qa-owner-uid',
|
|
email: 'qa-owner@motorsignal.example',
|
|
displayName: 'QA Owner',
|
|
isAnonymous: false,
|
|
);
|
|
|
|
const memberUser = AuthUser(
|
|
uid: 'qa-member-uid',
|
|
email: 'qa-member@motorsignal.example',
|
|
displayName: 'QA Member',
|
|
isAnonymous: false,
|
|
);
|
|
|
|
final ownerProfile = RiderProfile(
|
|
uid: 'qa-owner-uid',
|
|
handle: 'qa-owner',
|
|
displayName: 'QA Owner (Ride Lead)',
|
|
bio: 'Pacesetter and group organizer.',
|
|
region: 'North Georgia Mountains',
|
|
avatarPreset: 'preset_neon_cyan',
|
|
privacySettings: const ProfilePrivacySettings(
|
|
isSearchableByHandle: true,
|
|
isProfilePublic: true,
|
|
allowFriendRequests: true,
|
|
),
|
|
createdAt: DateTime(2026, 1, 1),
|
|
updatedAt: DateTime(2026, 1, 1),
|
|
);
|
|
|
|
final memberProfile = RiderProfile(
|
|
uid: 'qa-member-uid',
|
|
handle: 'qa-member',
|
|
displayName: 'QA Member (Participant)',
|
|
bio: 'Weekend enthusiast.',
|
|
region: 'North Georgia Mountains',
|
|
avatarPreset: 'preset_hyper_purple',
|
|
privacySettings: const ProfilePrivacySettings(
|
|
isSearchableByHandle: true,
|
|
isProfilePublic: true,
|
|
allowFriendRequests: true,
|
|
),
|
|
createdAt: DateTime(2026, 2, 1),
|
|
updatedAt: DateTime(2026, 2, 1),
|
|
);
|
|
|
|
final now = DateTime(2026, 10, 6, 10, 0);
|
|
|
|
final ride1 = GroupRide(
|
|
id: 'qa-e2e-join-approval-ride',
|
|
organizerUid: 'qa-owner-uid',
|
|
organizerHandle: 'qa-owner',
|
|
organizerDisplayName: 'QA Owner',
|
|
title: 'Blood Mountain Sweep',
|
|
description: 'Scenic group ride through Neels Gap and Vogel State Park.',
|
|
scheduledStartAt: now.add(const Duration(hours: 2)),
|
|
departureAt: now.add(const Duration(hours: 2, minutes: 30)),
|
|
status: GroupRideStatus.scheduled,
|
|
joinPolicy: GroupRideJoinPolicy.requestToJoin,
|
|
visibility: GroupRideVisibility.public,
|
|
participantLimit: 12,
|
|
participantCount: 3,
|
|
accessUids: const ['qa-owner-uid', 'qa-member-uid', 'qa-admin-uid'],
|
|
meetingPoint: const GroupRideLocation(
|
|
latitude: 34.5268,
|
|
longitude: -83.9849,
|
|
displayName: 'Dahlonega Square',
|
|
address: 'Public Square, Dahlonega, GA 30533',
|
|
locationType: GroupRideLocationType.meetup,
|
|
),
|
|
destination: const GroupRideLocation(
|
|
latitude: 34.7644,
|
|
longitude: -83.9248,
|
|
displayName: 'Neels Gap / Vogel State Park',
|
|
address: 'Blairsville, GA',
|
|
locationType: GroupRideLocationType.destination,
|
|
),
|
|
stops: const [
|
|
GroupRideLocation(
|
|
latitude: 34.6710,
|
|
longitude: -83.9180,
|
|
displayName: 'Mountain Crossing Viewpoint',
|
|
locationType: GroupRideLocationType.scenic,
|
|
),
|
|
],
|
|
createdAt: now.subtract(const Duration(days: 1)),
|
|
updatedAt: now,
|
|
);
|
|
|
|
final ride2 = GroupRide(
|
|
id: 'qa-member-georgia-twisties',
|
|
organizerUid: 'qa-member-uid',
|
|
organizerHandle: 'qa-member',
|
|
organizerDisplayName: 'QA Member',
|
|
title: 'North Georgia Sunday Twisties',
|
|
description: 'Scenic sweepers through Suches and Blood Mountain.',
|
|
scheduledStartAt: now.add(const Duration(days: 1)),
|
|
departureAt: now.add(const Duration(days: 1, minutes: 30)),
|
|
status: GroupRideStatus.scheduled,
|
|
joinPolicy: GroupRideJoinPolicy.open,
|
|
visibility: GroupRideVisibility.public,
|
|
participantLimit: 8,
|
|
participantCount: 2,
|
|
accessUids: const ['qa-member-uid', 'qa-owner-uid'],
|
|
meetingPoint: const GroupRideLocation(
|
|
latitude: 34.6895,
|
|
longitude: -84.0227,
|
|
displayName: 'Two Wheels of Suches',
|
|
address: '1915 GA-60, Suches, GA 30572',
|
|
locationType: GroupRideLocationType.meetup,
|
|
),
|
|
destination: const GroupRideLocation(
|
|
latitude: 34.7644,
|
|
longitude: -83.9248,
|
|
displayName: 'Vogel State Park',
|
|
address: 'Blairsville, GA',
|
|
locationType: GroupRideLocationType.destination,
|
|
),
|
|
stops: const [],
|
|
createdAt: now.subtract(const Duration(hours: 12)),
|
|
updatedAt: now,
|
|
);
|
|
|
|
const phoneViewport = Size(448, 997); // Pixel 8 Pro logical size
|
|
|
|
group('GroupRidesHubScreen Populated Hub Evidence & Captures', () {
|
|
testWidgets('Populated Hub - Organizer Account (Night Mode)', (tester) async {
|
|
tester.view.physicalSize = const Size(1344, 2992);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
final fakeAuth = _FakeAuth(user: ownerUser);
|
|
final repaintKey = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(fakeAuth),
|
|
currentUserProvider.overrideWithValue(ownerUser),
|
|
myRiderProfileProvider.overrideWith(
|
|
(ref) => Stream.value(ownerProfile),
|
|
),
|
|
upcomingGroupRidesProvider.overrideWith(
|
|
(ref) => Stream.value([ride1, ride2]),
|
|
),
|
|
myGroupRidesProvider.overrideWith(
|
|
(ref) => Stream.value([ride1]),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: RepaintBoundary(
|
|
key: repaintKey,
|
|
child: const GroupRidesHubScreen(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('GROUP RIDES'), findsOneWidget);
|
|
expect(find.text('@qa-owner'), findsOneWidget);
|
|
expect(find.text('Blood Mountain Sweep'), findsOneWidget);
|
|
expect(find.text('North Georgia Sunday Twisties'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await _captureScreen(
|
|
tester,
|
|
repaintKey,
|
|
'docs/design/motorsignal-hybrid-v1/screenshots/checkpoint_25_hub_organizer_night.png',
|
|
);
|
|
});
|
|
|
|
testWidgets('Populated Hub - Organizer Account (Day Mode)', (tester) async {
|
|
tester.view.physicalSize = const Size(1344, 2992);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
final fakeAuth = _FakeAuth(user: ownerUser);
|
|
final repaintKey = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(fakeAuth),
|
|
currentUserProvider.overrideWithValue(ownerUser),
|
|
myRiderProfileProvider.overrideWith(
|
|
(ref) => Stream.value(ownerProfile),
|
|
),
|
|
upcomingGroupRidesProvider.overrideWith(
|
|
(ref) => Stream.value([ride1, ride2]),
|
|
),
|
|
myGroupRidesProvider.overrideWith(
|
|
(ref) => Stream.value([ride1]),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.lightTheme,
|
|
home: RepaintBoundary(
|
|
key: repaintKey,
|
|
child: const GroupRidesHubScreen(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('GROUP RIDES'), findsOneWidget);
|
|
expect(find.text('Blood Mountain Sweep'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await _captureScreen(
|
|
tester,
|
|
repaintKey,
|
|
'docs/design/motorsignal-hybrid-v1/screenshots/checkpoint_26_hub_organizer_day.png',
|
|
);
|
|
});
|
|
|
|
testWidgets('Populated Hub - Organizer Account (Large Text 200% Scale)', (tester) async {
|
|
tester.view.physicalSize = const Size(1344, 2992);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
final fakeAuth = _FakeAuth(user: ownerUser);
|
|
final repaintKey = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(fakeAuth),
|
|
currentUserProvider.overrideWithValue(ownerUser),
|
|
myRiderProfileProvider.overrideWith(
|
|
(ref) => Stream.value(ownerProfile),
|
|
),
|
|
upcomingGroupRidesProvider.overrideWith(
|
|
(ref) => Stream.value([ride1, ride2]),
|
|
),
|
|
myGroupRidesProvider.overrideWith(
|
|
(ref) => Stream.value([ride1]),
|
|
),
|
|
],
|
|
child: MediaQuery(
|
|
data: const MediaQueryData(
|
|
size: phoneViewport,
|
|
textScaler: TextScaler.linear(2.0),
|
|
),
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: RepaintBoundary(
|
|
key: repaintKey,
|
|
child: const GroupRidesHubScreen(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('GROUP RIDES'), findsOneWidget);
|
|
expect(find.text('Blood Mountain Sweep'), findsOneWidget);
|
|
final exception = tester.takeException();
|
|
if (exception is FlutterError) {
|
|
debugPrint(exception.toStringDeep());
|
|
}
|
|
expect(exception, isNull);
|
|
|
|
await _captureScreen(
|
|
tester,
|
|
repaintKey,
|
|
'docs/design/motorsignal-hybrid-v1/screenshots/checkpoint_27_hub_organizer_large_text.png',
|
|
);
|
|
});
|
|
|
|
testWidgets('Populated Hub - Participant Account (Night Mode)', (tester) async {
|
|
tester.view.physicalSize = const Size(1344, 2992);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
|
|
final fakeAuth = _FakeAuth(user: memberUser);
|
|
final repaintKey = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
authServiceProvider.overrideWithValue(fakeAuth),
|
|
currentUserProvider.overrideWithValue(memberUser),
|
|
myRiderProfileProvider.overrideWith(
|
|
(ref) => Stream.value(memberProfile),
|
|
),
|
|
upcomingGroupRidesProvider.overrideWith(
|
|
(ref) => Stream.value([ride1, ride2]),
|
|
),
|
|
myGroupRidesProvider.overrideWith(
|
|
(ref) => Stream.value([ride2]),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: RepaintBoundary(
|
|
key: repaintKey,
|
|
child: const GroupRidesHubScreen(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('GROUP RIDES'), findsOneWidget);
|
|
expect(find.text('@qa-member'), findsOneWidget);
|
|
expect(find.text('Blood Mountain Sweep'), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await _captureScreen(
|
|
tester,
|
|
repaintKey,
|
|
'docs/design/motorsignal-hybrid-v1/screenshots/checkpoint_28_hub_participant.png',
|
|
);
|
|
});
|
|
});
|
|
}
|