import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:drift/native.dart'; import 'package:geolocator/geolocator.dart'; import 'package:drift/drift.dart' as drift; import 'package:motorcycle_dash/core/router.dart'; import 'package:motorcycle_dash/core/theme/theme.dart'; import 'package:motorcycle_dash/core/widgets/main_layout.dart'; import 'package:motorcycle_dash/database/database.dart' hide FieldTestEvent, FieldTestSummary, TelemetrySample; import 'package:motorcycle_dash/models/field_test_event.dart'; import 'package:motorcycle_dash/features/dashboard/dashboard_screen.dart'; import 'package:motorcycle_dash/features/map/map_screen.dart'; import 'package:motorcycle_dash/features/history/history_screen.dart'; import 'package:motorcycle_dash/features/history/ride_detail_screen.dart'; import 'package:motorcycle_dash/features/records/records_screen.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/features/garage/garage_screen.dart'; import 'package:motorcycle_dash/features/garage/add_motorcycle_screen.dart'; import 'package:motorcycle_dash/features/garage/motorcycle_detail_screen.dart'; import 'package:motorcycle_dash/features/garage/garage_providers.dart'; import 'package:motorcycle_dash/features/settings/settings_screen.dart'; import 'package:motorcycle_dash/features/settings/account_screen.dart'; import 'package:motorcycle_dash/features/entitlements/pro_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/features/settings/diagnostics_screen.dart'; import 'package:motorcycle_dash/features/settings/field_test_live_log_screen.dart'; import 'package:motorcycle_dash/features/settings/field_test_report_screen.dart'; import 'package:motorcycle_dash/features/field_test/field_test_pre_ride_check_screen.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/routes/route_planner_screen.dart'; import 'package:motorcycle_dash/features/routes/route_detail_screen.dart'; import 'package:motorcycle_dash/features/rides/ride_state_machine.dart'; import 'package:motorcycle_dash/models/motorcycle_models.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_service.dart'; import 'package:motorcycle_dash/services/motorcycle/motorcycle_repository.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/billing/billing_providers.dart'; import 'package:motorcycle_dash/services/billing/fake_billing_service.dart'; import 'package:motorcycle_dash/services/entitlements/entitlement_providers.dart'; import 'package:motorcycle_dash/services/entitlements/entitlement_storage.dart'; import 'package:motorcycle_dash/services/field_test/field_test_logger.dart'; import 'package:motorcycle_dash/services/location_service.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/group_ride_service.dart'; import 'package:motorcycle_dash/services/routes/route_models.dart'; import 'package:motorcycle_dash/services/routes/route_providers.dart'; import 'package:motorcycle_dash/services/routes/route_service.dart'; import 'package:motorcycle_dash/services/social/rider_profile_models.dart'; import 'package:motorcycle_dash/services/social/social_providers.dart'; import 'package:motorcycle_dash/services/map/map_providers.dart'; import 'package:motorcycle_dash/services/map/map_provider_interface.dart'; // Canonical 9-viewport regression matrix definitions const Map canonicalViewports = { 'compact_portrait_320x640': Size(320, 640), 'standard_portrait_400x800': Size(400, 800), 'pixel7_portrait_412x915': Size(412, 915), 'compact_landscape_640x320': Size(640, 320), 'short_landscape_780x360': Size(780, 360), 'standard_landscape_800x360': Size(800, 360), 'wide_landscape_840x390': Size(840, 390), 'portrait_tablet_768x1024': Size(768, 1024), 'landscape_tablet_1024x768': Size(1024, 768), }; // IME virtual keyboard inset viewports const Map imeViewports = { 'compact_portrait_ime': Size(320, 640), 'standard_portrait_ime': Size(400, 800), 'compact_landscape_ime': Size(640, 320), }; class _FakeAuth implements AuthService { @override Future ensureBackendIdentity() async {} AuthUser? user; final _states = StreamController.broadcast(); _FakeAuth({this.user}); @override Stream 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 => user?.emailVerified ?? false; @override Future deleteCurrentUser() async { user = null; _states.add(currentState); } @override void dispose() => _states.close(); @override Future getIdToken([bool forceRefresh = false]) async => 'fake-token'; @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _FakeLocationService extends ChangeNotifier implements LocationService { final StreamController _controller = StreamController.broadcast(); @override Stream getFilteredPositionStream() => _controller.stream; @override Future requestPermissions() async => true; @override Future hasPermissions() async => true; @override void Function(Position, bool, String?)? onGpsDiagnostic; @override void dispose() { _controller.close(); super.dispose(); } @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _FakeMotorcycleRepository implements MotorcycleRepository { @override Future> getYears() async => [MotorcycleYear(2024)]; @override Future> getMakes(int year) async => [ MotorcycleMake(id: 'triumph', name: 'Triumph'), ]; @override Future> getModels(int year, String makeId) async => [ MotorcycleModel( id: 'speed-triple', name: 'Speed Triple 1200 RS', year: 2024, makeId: 'triumph', ), ]; @override Future> getVariants(int year, String makeId, String modelId) async => []; @override Future getSpecifications(MotorcycleSelection selection) async => null; } class _FakeFieldTestLogger implements FieldTestLogger { @override int? get activeRideId => null; @override bool get isSessionActive => false; @override DateTime? get sessionStartTime => null; @override double get currentQualityEstimate => 100.0; @override double get currentSampleRateHz => 20.0; @override double get lastGpsAccuracy => 2.5; @override int get lastGpsAgeMs => 50; @override double get averageTimingJitterMs => 1.2; @override int get currentQueueDepth => 0; @override int get peakQueueDepth => 0; @override int get queueCapacity => 1000; @override int get totalEnqueued => 0; @override int get totalFlushed => 0; @override int get droppedEventsCount => 0; @override String? get oldestDroppedItem => null; @override String? get newestDroppedItem => null; @override int get lastFlushDurationMs => 0; @override int get failedFlushCount => 0; int get telemetryGapCount => 0; int get gpsStaleCount => 0; int get gpsJumpRejectCount => 0; int get imuSaturationCount => 0; int get accelerationEventCount => 0; int get brakingEventCount => 0; @override List get timelineEvents => const []; @override Stream> get timelineStream => const Stream.empty(); @override void dispose() {} @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _FakeGroupRideService implements GroupRideService { @override Stream groupRideStream(String groupRideId) => watchGroupRide(groupRideId); @override Stream> participantsStream(String rideId) => watchParticipants(rideId); Stream watchGroupRide(String rideId) { return Stream.value(GroupRide( id: rideId, organizerUid: 'user-1', organizerHandle: 'canyon_lead', organizerDisplayName: 'Canyon Lead', title: 'Weekend Mountain Ride', description: 'Twisty canyon roads and scenic outlooks', meetingPoint: const GroupRideLocation( latitude: 37.0, longitude: -122.0, displayName: 'Mountain Viewpoint', locationType: GroupRideLocationType.meetup, ), scheduledStartAt: DateTime.now().add(const Duration(hours: 2)), departureAt: DateTime.now().add(const Duration(hours: 2, minutes: 30)), visibility: GroupRideVisibility.public, joinPolicy: GroupRideJoinPolicy.open, status: GroupRideStatus.scheduled, participantCount: 3, createdAt: DateTime.now(), updatedAt: DateTime.now(), )); } Stream> watchUpcomingRides() { return Stream.value([ GroupRide( id: 'ride-1', organizerUid: 'user-1', organizerHandle: 'canyon_lead', organizerDisplayName: 'Canyon Lead', title: 'Morning Cruise', description: 'Casual morning cruise through the hills', meetingPoint: const GroupRideLocation( latitude: 37.0, longitude: -122.0, displayName: 'Basecamp', locationType: GroupRideLocationType.meetup, ), scheduledStartAt: DateTime.now().add(const Duration(hours: 4)), departureAt: DateTime.now().add(const Duration(hours: 4, minutes: 30)), visibility: GroupRideVisibility.public, joinPolicy: GroupRideJoinPolicy.open, status: GroupRideStatus.scheduled, participantCount: 4, createdAt: DateTime.now(), updatedAt: DateTime.now(), ), ]); } Stream> watchActiveRides() => Stream.value([]); Stream> watchMyRides() => Stream.value([]); @override Stream> upcomingRidesStream({required String callerUid}) => watchUpcomingRides(); @override Stream> myRidesStream({required String callerUid}) => watchMyRides(); Stream> watchParticipants(String rideId) => Stream.value([ GroupRideParticipant( userId: 'user-1', riderHandle: 'lead_rider', riderDisplayName: 'Leader Rider', role: GroupRideRole.lead, status: ParticipantStatus.accepted, createdAt: DateTime.now(), updatedAt: DateTime.now(), ), ]); @override Stream participantDocStream({ required String groupRideId, required String participantUid, }) => Stream.value(null); @override Stream livePresenceDocStream({ required String groupRideId, required String targetUid, }) => Stream.value(null); @override Future clearLivePresence({ required String callerUid, required String groupRideId, }) async {} Future updateLivePresence({ required String callerUid, required String groupRideId, required LiveGroupPresence presence, }) async {} @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _FakeRouteService implements RouteService { final Map _routes = { 'route-1': MotorSignalRoute( id: 'route-1', ownerUid: 'user-1', ownerHandle: 'testrider', title: 'Pacific Coast Highway', description: 'Scenic ocean run', visibility: RouteVisibility.public, stops: const [ RouteStop( id: 's1', order: 0, type: RouteStopType.start, displayName: 'Santa Monica Pier', latitude: 34.01, longitude: -118.49, address: 'Santa Monica, CA', notes: 'Meet at pier entrance', ), RouteStop( id: 's2', order: 1, type: RouteStopType.destination, displayName: 'Malibu Overlook', latitude: 34.03, longitude: -118.68, address: 'Malibu, CA', notes: 'End of ride photo spot', ), ], straightLineDistanceMeters: 25000, source: RouteSource.planner, version: 1, createdAt: DateTime.now(), updatedAt: DateTime.now(), ), }; @override Future getRoute(String routeId) async => _routes[routeId]; Stream watchRoute(String routeId) => Stream.value(_routes[routeId]); @override Stream routeStream(String routeId) => watchRoute(routeId); Stream> watchMyRoutes() => Stream.value(_routes.values.toList()); Stream> watchPublicRoutes() => Stream.value(_routes.values.toList()); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _FakeRideStateMachine extends StateNotifier implements RideStateMachine { _FakeRideStateMachine(super.initialState); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } class _FakeMapProvider implements AppMapProvider { @override AppMapType get type => AppMapType.openStreetMap; @override String get name => 'FakeMap'; @override Widget buildMapView({ required BuildContext context, required AppLatLng center, required double zoom, double bearing = 0.0, required List markers, required List polylines, required bool isFollowMode, required MapConfig config, required ValueChanged? onPositionChanged, required VoidCallback? onUserInteraction, }) { return Container( key: const ValueKey('fake_map_view'), color: Colors.black, child: const Center(child: Text('MAP_VIEW')), ); } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); late AppDatabase db; late SharedPreferences prefs; late _FakeAuth authService; late FakeBillingService billingService; late InMemoryEntitlementStorage entitlementStorage; late _FakeGroupRideService groupRideService; late _FakeRouteService routeService; late _FakeLocationService locationService; late _FakeMotorcycleRepository motorcycleRepo; late _FakeFieldTestLogger fieldTestLogger; setUp(() async { db = AppDatabase.forTesting(NativeDatabase.memory()); SharedPreferences.setMockInitialValues({ 'app_theme_mode': 'dark', }); prefs = await SharedPreferences.getInstance(); authService = _FakeAuth( user: const AuthUser( uid: 'test-user-123', email: 'rider@example.com', displayName: 'Test Rider', ), ); billingService = FakeBillingService(); entitlementStorage = InMemoryEntitlementStorage(); groupRideService = _FakeGroupRideService(); routeService = _FakeRouteService(); locationService = _FakeLocationService(); motorcycleRepo = _FakeMotorcycleRepository(); fieldTestLogger = _FakeFieldTestLogger(); // Populate a test motorcycle in Drift DB await db.into(db.garageMotorcycles).insert( GarageMotorcyclesCompanion.insert( year: 2024, makeId: 'triumph', makeName: 'Triumph', modelId: 'speed-triple', modelName: 'Speed Triple 1200 RS', dateAdded: DateTime.now(), lastUpdated: DateTime.now(), isDefault: const drift.Value(true), ), ); // Populate a completed test ride in Drift DB await db.into(db.rides).insert( RidesCompanion.insert( startTime: DateTime.now().subtract(const Duration(hours: 1)), endTime: drift.Value(DateTime.now().subtract(const Duration(minutes: 10))), distanceMiles: const drift.Value(24.5), movingTime: const drift.Value(1800), topSpeedMph: const drift.Value(72.4), maxLeanLeft: const drift.Value(42.1), maxLeanRight: const drift.Value(44.8), maxBrakingG: const drift.Value(0.88), maxAccelG: const drift.Value(0.65), ), ); }); tearDown(() async { locationService.dispose(); authService.dispose(); await db.close(); }); Widget buildTestApp({ required Widget child, ThemeMode themeMode = ThemeMode.dark, AuthUser? customUser, }) { final activeAuth = customUser != null ? _FakeAuth(user: customUser) : authService; return ProviderScope( overrides: [ databaseProvider.overrideWithValue(db), sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(activeAuth), billingServiceProvider.overrideWithValue(billingService), entitlementStorageProvider.overrideWithValue(entitlementStorage), groupRideServiceProvider.overrideWithValue(groupRideService), routeServiceProvider.overrideWithValue(routeService), activeMapProvider.overrideWithValue(_FakeMapProvider()), locationServiceProvider.overrideWithValue(locationService), motorcycleRepositoryProvider.overrideWithValue(motorcycleRepo), fieldTestLoggerProvider.overrideWithValue(fieldTestLogger), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])), locationPermissionGrantedProvider.overrideWith((ref) => Future.value(true)), myRiderProfileProvider.overrideWith((ref) => Stream.value( RiderProfile( uid: 'user-1', handle: 'testrider', displayName: 'Test Rider', bio: 'Motorcycle enthusiast', createdAt: DateTime.now(), updatedAt: DateTime.now(), ), )), myFriendsProvider.overrideWith((ref) => Stream.value([])), myIncomingRequestsProvider.overrideWith((ref) => Stream.value([])), myOutgoingRequestsProvider.overrideWith((ref) => Stream.value([])), ], child: MaterialApp( debugShowCheckedModeBanner: false, theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: themeMode, home: Scaffold(body: child), ), ); } Future pumpView(WidgetTester tester, Widget app) async { await tester.pumpWidget(app); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); } Future unmountView(WidgetTester tester) async { await tester.pumpWidget(const SizedBox()); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); } // ========================================================================= // 1. WHOLE-APP CANONICAL 9-VIEWPORT RESPONSIVE MATRIX // ========================================================================= group('UI-2I Whole-App 9-Viewport Responsive Matrix', () { testWidgets('DashboardScreen renders cleanly across all 9 canonical viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const DashboardScreen())); expect(find.byType(DashboardScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('MapScreen renders cleanly across all 9 canonical viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const MapScreen())); expect(find.byType(MapScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('HistoryScreen renders cleanly across all 9 canonical viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const HistoryScreen())); expect(find.byType(HistoryScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('RecordsScreen renders cleanly across all 9 canonical viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const RecordsScreen())); expect(find.byType(RecordsScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('GarageScreen renders cleanly across all 9 canonical viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const GarageScreen())); expect(find.byType(GarageScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('SettingsScreen renders cleanly across all 9 canonical viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const SettingsScreen())); expect(find.byType(SettingsScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('AccountScreen (authenticated) renders cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const AccountScreen())); expect(find.byType(AccountScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('AccountScreen (unauthenticated) renders cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const AccountScreen(), customUser: null)); expect(find.byType(AccountScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('ProScreen renders cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const ProScreen())); expect(find.byType(ProScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('DiagnosticsScreen renders cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const DiagnosticsScreen())); expect(find.byType(DiagnosticsScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('FieldTestPreRideCheckScreen renders cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const FieldTestPreRideCheckScreen())); expect(find.byType(FieldTestPreRideCheckScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('FieldTestLiveLogScreen renders cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const FieldTestLiveLogScreen())); expect(find.byType(FieldTestLiveLogScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('FieldTestReportScreen renders cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const FieldTestReportScreen())); expect(find.byType(FieldTestReportScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('GroupRidesHubScreen renders cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const GroupRidesHubScreen())); expect(find.byType(GroupRidesHubScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('CreateGroupRideScreen renders cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const CreateGroupRideScreen())); expect(find.byType(CreateGroupRideScreen), findsOneWidget, reason: 'Failed on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on ${entry.key}'); } await unmountView(tester); }); testWidgets('GroupRideDetailScreen & LiveGroupRideScreen render cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const GroupRideDetailScreen(rideId: 'ride-1'))); expect(find.byType(GroupRideDetailScreen), findsOneWidget, reason: 'Failed Detail on ${entry.key}'); await pumpView(tester, buildTestApp(child: const LiveGroupRideScreen(rideId: 'ride-1'))); expect(find.byType(LiveGroupRideScreen), findsOneWidget, reason: 'Failed Live on ${entry.key}'); expect(tester.takeException(), isNull); } await unmountView(tester); }); testWidgets('RoutePlannerScreen & RouteDetailScreen render cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const RoutePlannerScreen())); expect(find.byType(RoutePlannerScreen), findsOneWidget, reason: 'Failed Planner on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on RoutePlanner on ${entry.key}'); await pumpView(tester, buildTestApp(child: const RouteDetailScreen(routeId: 'route-1'))); expect(find.byType(RouteDetailScreen), findsOneWidget, reason: 'Failed RouteDetail on ${entry.key}'); expect(tester.takeException(), isNull, reason: 'Exception on RouteDetail on ${entry.key}'); } await unmountView(tester); }); testWidgets('AddMotorcycleScreen & MotorcycleDetailScreen render cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const AddMotorcycleScreen())); expect(find.byType(AddMotorcycleScreen), findsOneWidget, reason: 'Failed AddMotorcycle on ${entry.key}'); await pumpView(tester, buildTestApp(child: const MotorcycleDetailScreen(motorcycleId: 1))); expect(find.byType(MotorcycleDetailScreen), findsOneWidget, reason: 'Failed MotorcycleDetail on ${entry.key}'); expect(tester.takeException(), isNull); } await unmountView(tester); }); testWidgets('RiderProfileScreen & FriendsScreen render cleanly across all 9 viewports', (tester) async { for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: const RiderProfileScreen())); expect(find.byType(RiderProfileScreen), findsOneWidget, reason: 'Failed RiderProfile on ${entry.key}'); await pumpView(tester, buildTestApp(child: const FriendsScreen())); expect(find.byType(FriendsScreen), findsOneWidget, reason: 'Failed FriendsScreen on ${entry.key}'); expect(tester.takeException(), isNull); } await unmountView(tester); }); testWidgets('RideDetailScreen renders cleanly across all 9 viewports', (tester) async { final ride = Ride( id: 1, startTime: DateTime.now().subtract(const Duration(hours: 1)), endTime: DateTime.now().subtract(const Duration(minutes: 10)), distanceMiles: 24.5, movingTime: 1800, topSpeedMph: 72.4, maxLeanLeft: 42.1, maxLeanRight: 44.8, maxBrakingG: 0.88, maxAccelG: 0.65, distance: 24.5, stoppedTime: 100, averageSpeed: 48.0, topSpeed: 72.4, elevationGain: 350.0, elevationLoss: 320.0, ); for (final entry in canonicalViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView(tester, buildTestApp(child: RideDetailScreen(ride: ride))); expect(find.byType(RideDetailScreen), findsOneWidget, reason: 'Failed RideDetail on ${entry.key}'); expect(tester.takeException(), isNull); } await unmountView(tester); }); }); // ========================================================================= // 2. IME KEYBOARD INSET RESILIENCY MATRIX // ========================================================================= group('UI-2I IME Keyboard-Sensitive Form Resiliency Matrix', () { testWidgets('AddMotorcycleScreen remains scrollable and submit reachable with virtual keyboard', (tester) async { for (final entry in imeViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; tester.view.viewPadding = const FakeViewPadding(bottom: 300); addTearDown(() { tester.view.resetPhysicalSize(); tester.view.resetViewPadding(); }); await pumpView(tester, buildTestApp(child: const AddMotorcycleScreen())); expect(find.byType(AddMotorcycleScreen), findsOneWidget); expect(tester.takeException(), isNull, reason: 'Overflow on ${entry.key} with keyboard'); } await unmountView(tester); }); testWidgets('CreateGroupRideScreen remains scrollable with virtual keyboard', (tester) async { for (final entry in imeViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; tester.view.viewPadding = const FakeViewPadding(bottom: 300); addTearDown(() { tester.view.resetPhysicalSize(); tester.view.resetViewPadding(); }); await pumpView(tester, buildTestApp(child: const CreateGroupRideScreen())); expect(find.byType(CreateGroupRideScreen), findsOneWidget); expect(tester.takeException(), isNull, reason: 'Overflow on ${entry.key} with keyboard'); } await unmountView(tester); }); testWidgets('AccountScreen auth form remains scrollable with virtual keyboard', (tester) async { for (final entry in imeViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; tester.view.viewPadding = const FakeViewPadding(bottom: 300); addTearDown(() { tester.view.resetPhysicalSize(); tester.view.resetViewPadding(); }); await pumpView(tester, buildTestApp( child: const AccountScreen(), customUser: null, )); expect(find.byType(AccountScreen), findsOneWidget); expect(tester.takeException(), isNull, reason: 'Overflow on ${entry.key} with keyboard'); } await unmountView(tester); }); testWidgets('RoutePlannerScreen search & stop input remains reachable with virtual keyboard', (tester) async { for (final entry in imeViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; tester.view.viewPadding = const FakeViewPadding(bottom: 300); addTearDown(() { tester.view.resetPhysicalSize(); tester.view.resetViewPadding(); }); await pumpView(tester, buildTestApp(child: const RoutePlannerScreen())); expect(find.byType(RoutePlannerScreen), findsOneWidget); expect(tester.takeException(), isNull, reason: 'Overflow on ${entry.key} with keyboard'); } await unmountView(tester); }); testWidgets('RiderProfileScreen handle & bio inputs remain reachable with virtual keyboard', (tester) async { for (final entry in imeViewports.entries) { tester.view.physicalSize = entry.value; tester.view.devicePixelRatio = 1.0; tester.view.viewPadding = const FakeViewPadding(bottom: 300); addTearDown(() { tester.view.resetPhysicalSize(); tester.view.resetViewPadding(); }); await pumpView(tester, buildTestApp(child: const RiderProfileScreen())); expect(find.byType(RiderProfileScreen), findsOneWidget); expect(tester.takeException(), isNull, reason: 'Overflow on ${entry.key} with keyboard'); } await unmountView(tester); }); }); // ========================================================================= // 3. THEME SWITCHING & STATE PRESERVATION MATRIX // ========================================================================= group('UI-2I Theme Switching & State Preservation Matrix', () { testWidgets('Seamless Theme Transition Dark -> Light -> System -> Dark preserves screen state', (tester) async { tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); final themeModes = [ThemeMode.dark, ThemeMode.light, ThemeMode.system, ThemeMode.dark]; for (final mode in themeModes) { await pumpView(tester, buildTestApp( child: const DashboardScreen(), themeMode: mode, )); expect(find.byType(DashboardScreen), findsOneWidget); expect(tester.takeException(), isNull); } await unmountView(tester); }); testWidgets('MainLayout navigation shell remains intact when resizing across breakpoints', (tester) async { final sizes = [ const Size(320, 640), // Compact const Size(640, 320), // Compact Landscape const Size(768, 1024), // Medium Tablet const Size(1024, 768), // Expanded Tablet const Size(400, 800), // Standard Phone ]; for (final size in sizes) { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); await pumpView( tester, ProviderScope( overrides: [ databaseProvider.overrideWithValue(db), sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(authService), billingServiceProvider.overrideWithValue(billingService), entitlementStorageProvider.overrideWithValue(entitlementStorage), groupRideServiceProvider.overrideWithValue(groupRideService), routeServiceProvider.overrideWithValue(routeService), activeMapProvider.overrideWithValue(_FakeMapProvider()), locationServiceProvider.overrideWithValue(locationService), motorcycleRepositoryProvider.overrideWithValue(motorcycleRepo), fieldTestLoggerProvider.overrideWithValue(fieldTestLogger), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])), locationPermissionGrantedProvider.overrideWith((ref) => Future.value(true)), ], child: MaterialApp.router( routerConfig: appRouter, theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: ThemeMode.dark, ), ), ); expect(find.byType(MainLayout), findsOneWidget, reason: 'Failed MainLayout at $size'); expect(tester.takeException(), isNull); } await unmountView(tester); }); }); // ========================================================================= // 4. RIDER SAFETY UI GATES VERIFICATION // ========================================================================= group('UI-2I Rider Safety UI Gate Invariants', () { testWidgets('Active recording ride does not trigger disruptive modal sheets', (tester) async { tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); final activeSession = RideSession( state: RideState.riding, startTime: DateTime.now().subtract(const Duration(minutes: 5)), ); await pumpView( tester, ProviderScope( overrides: [ databaseProvider.overrideWithValue(db), sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(authService), billingServiceProvider.overrideWithValue(billingService), entitlementStorageProvider.overrideWithValue(entitlementStorage), groupRideServiceProvider.overrideWithValue(groupRideService), routeServiceProvider.overrideWithValue(routeService), activeMapProvider.overrideWithValue(_FakeMapProvider()), locationServiceProvider.overrideWithValue(locationService), motorcycleRepositoryProvider.overrideWithValue(motorcycleRepo), fieldTestLoggerProvider.overrideWithValue(fieldTestLogger), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])), locationPermissionGrantedProvider.overrideWith((ref) => Future.value(true)), rideStateMachineProvider.overrideWith((ref) => _FakeRideStateMachine(activeSession)), ], child: MaterialApp( debugShowCheckedModeBanner: false, theme: AppTheme.darkTheme, home: const Scaffold(body: DashboardScreen()), ), ), ); // Ensure no BottomSheet or AlertDialog is automatically presented expect(find.byType(BottomSheet), findsNothing); expect(find.byType(AlertDialog), findsNothing); expect(find.byType(DashboardScreen), findsOneWidget); await unmountView(tester); }); }); // ========================================================================= // 5. NAVIGATION FLOWS & DEEP ROUTE TRANSITIONS // ========================================================================= group('UI-2I Navigation Flows & Deep Route Transitions', () { testWidgets('App shell navigates cleanly across all primary destinations', (tester) async { tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); final destinations = [ '/', '/map', '/history', '/records', '/garage', '/settings', '/', ]; for (final path in destinations) { appRouter.go(path); await pumpView( tester, ProviderScope( overrides: [ databaseProvider.overrideWithValue(db), sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(authService), billingServiceProvider.overrideWithValue(billingService), entitlementStorageProvider.overrideWithValue(entitlementStorage), groupRideServiceProvider.overrideWithValue(groupRideService), routeServiceProvider.overrideWithValue(routeService), activeMapProvider.overrideWithValue(_FakeMapProvider()), locationServiceProvider.overrideWithValue(locationService), motorcycleRepositoryProvider.overrideWithValue(motorcycleRepo), fieldTestLoggerProvider.overrideWithValue(fieldTestLogger), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])), locationPermissionGrantedProvider.overrideWith((ref) => Future.value(true)), ], child: MaterialApp.router( routerConfig: appRouter, theme: AppTheme.darkTheme, ), ), ); expect(tester.takeException(), isNull, reason: 'Navigation failed to $path'); } await unmountView(tester); }); testWidgets('Deep secondary route transitions render cleanly without exception', (tester) async { tester.view.physicalSize = const Size(400, 800); tester.view.devicePixelRatio = 1.0; addTearDown(() => tester.view.resetPhysicalSize()); final deepRoutes = [ '/history/1', '/garage/1', '/routes/route-1', '/group-rides/ride-1', '/settings/account', '/settings/pro', '/settings/diagnostics', '/settings/diagnostics/live-log', '/settings/diagnostics/report', ]; for (final path in deepRoutes) { appRouter.go(path); await pumpView( tester, ProviderScope( overrides: [ databaseProvider.overrideWithValue(db), sharedPreferencesProvider.overrideWithValue(prefs), authServiceProvider.overrideWithValue(authService), billingServiceProvider.overrideWithValue(billingService), entitlementStorageProvider.overrideWithValue(entitlementStorage), groupRideServiceProvider.overrideWithValue(groupRideService), routeServiceProvider.overrideWithValue(routeService), activeMapProvider.overrideWithValue(_FakeMapProvider()), locationServiceProvider.overrideWithValue(locationService), motorcycleRepositoryProvider.overrideWithValue(motorcycleRepo), fieldTestLoggerProvider.overrideWithValue(fieldTestLogger), activeAlertsStreamProvider.overrideWith((ref) => Stream.value([])), locationPermissionGrantedProvider.overrideWith((ref) => Future.value(true)), ], child: MaterialApp.router( routerConfig: appRouter, theme: AppTheme.darkTheme, ), ), ); expect(tester.takeException(), isNull, reason: 'Deep route failed for $path'); } await unmountView(tester); }); }); }