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/dashboard/dashboard_screen.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/g_force_indicator.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/gps_status_indicator.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/heading_indicator.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/lean_angle_indicator.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/performance_summary.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/responsive_dashboard_layout.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/ride_action_button.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/ride_status_chip.dart'; import 'package:motorcycle_dash/features/dashboard/widgets/speed_readout.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/features/rides/ride_state_machine.dart'; import 'package:motorcycle_dash/models/telemetry_sample.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_models.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_service.dart'; import 'package:motorcycle_dash/services/location_service.dart'; import 'package:motorcycle_dash/services/telemetry_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late SharedPreferences prefs; late StreamController telemetryController; late StreamController> alertsController; setUp(() async { SharedPreferences.setMockInitialValues({}); prefs = await SharedPreferences.getInstance(); telemetryController = StreamController.broadcast(); alertsController = StreamController>.broadcast(); }); tearDown(() async { await telemetryController.close(); await alertsController.close(); }); Widget buildTestDashboard({ RideSession? initialSession, List? initialAlerts, }) { return ProviderScope( overrides: [ sharedPreferencesProvider.overrideWithValue(prefs), telemetryStreamProvider.overrideWith((ref) => telemetryController.stream), activeAlertsStreamProvider.overrideWith((ref) => alertsController.stream), dashboardAlertsStreamProvider.overrideWith((ref) => alertsController.stream), locationPermissionGrantedProvider.overrideWith((ref) => true), ], child: MaterialApp( theme: AppTheme.darkTheme, home: const DashboardScreen(), ), ); } void configureViewport(WidgetTester tester, double width, double height) { tester.view.physicalSize = Size(width, height); tester.view.devicePixelRatio = 1.0; } group('Landscape Dashboard Redesign Matrix Tests', () { testWidgets('Renders adaptive 3-column composition on 840x390 landscape phone', (tester) async { configureViewport(tester, 840, 390); addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget(buildTestDashboard()); await tester.pump(); // Zero RenderFlex overflow expect(tester.takeException(), isNull); // Verify Left Column: G-Force indicator & statistics tiles expect(find.byType(GForceIndicator), findsOneWidget); expect(find.text('TRIP'), findsOneWidget); expect(find.text('TOP'), findsOneWidget); expect(find.text('AVG'), findsOneWidget); // Verify Center Column: Speedometer, Status row, Controls expect(find.byType(SpeedReadout), findsOneWidget); expect(find.byType(GPSStatusIndicator), findsOneWidget); expect(find.byType(HeadingIndicator), findsOneWidget); expect(find.byType(RideStatusChip), findsOneWidget); expect(find.byType(RideActionButton), findsOneWidget); expect(find.byType(PerformanceLaunchShortcut), findsOneWidget); // Verify Right Column: Lean-angle gauge & dynamics expect(find.byType(LeanAngleIndicator), findsOneWidget); expect(find.text('MOVING'), findsOneWidget); expect(find.text('ELEV'), findsOneWidget); expect(find.byType(PerformanceSummary), findsOneWidget); // Verify horizontal order: GForce (left) < Speed (center) < LeanAngle (right) final gForceCenter = tester.getCenter(find.byType(GForceIndicator)); final speedCenter = tester.getCenter(find.byType(SpeedReadout)); final leanCenter = tester.getCenter(find.byType(LeanAngleIndicator)); expect(gForceCenter.dx, lessThan(speedCenter.dx)); expect(speedCenter.dx, lessThan(leanCenter.dx)); }); testWidgets('Landscape alert banner is compact (<= 40dp height) and non-disruptive', (tester) async { configureViewport(tester, 840, 390); addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget(buildTestDashboard()); await tester.pump(); final now = DateTime.now(); final alert = RoadAlert( id: 'police_ahead_1', providerId: 'waze_zyla', category: RoadAlertCategory.police, latitude: 34.05, longitude: -84.40, distanceFromUser: 450, roadName: 'GA 400 Southbound', description: 'Police Speed Trap', reportedAt: now, lastUpdated: now, source: 'Waze', ); alertsController.add([alert]); await tester.pump(const Duration(milliseconds: 50)); expect(tester.takeException(), isNull); expect(find.text('POLICE'), findsOneWidget); expect(find.textContaining('0.3 mi Ahead'), findsOneWidget); // Verify banner height is constrained to <= 40dp final bannerFinder = find.byWidgetPredicate( (widget) => widget is Container && widget.constraints?.maxHeight == 38 || (widget is Container && widget.decoration is BoxDecoration && (widget.decoration as BoxDecoration).border?.bottom != null), ); expect(bannerFinder, findsWidgets); // Speedometer remains visible and unobstructed expect(find.byType(SpeedReadout), findsOneWidget); expect(tester.getCenter(find.byType(SpeedReadout)).dy, greaterThan(60)); }); testWidgets('Preserves clear margin on right edge (>= 16dp) in landscape for volume overlay', (tester) async { configureViewport(tester, 840, 390); addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget(buildTestDashboard()); await tester.pump(); // Find the ResponsiveDashboardLayout Padding widget final paddingFinder = find.ancestor( of: find.byType(ResponsiveDashboardLayout), matching: find.byType(Padding), ); expect(paddingFinder, findsWidgets); final paddingWidget = tester.firstWidget(find.descendant( of: find.byType(ResponsiveDashboardLayout), matching: find.byType(Padding), )); final edgeInsets = paddingWidget.padding as EdgeInsets; expect(edgeInsets.right, greaterThanOrEqualTo(16.0)); }); }); }