MotorSignal-App/test/render_hub_screenshots_test...

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