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:motorcycle_dash/services/alerts/widgets/quick_swarm_report_modal.dart'; import 'package:motorcycle_dash/features/social/feed/widgets/quick_squad_feed_button.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)); }); testWidgets('BUG-007: Short landscape cockpit mount includes incident report and squad buttons with balanced layout', (tester) async { configureViewport(tester, 800, 360); addTearDown(() => tester.view.resetPhysicalSize()); await tester.pumpWidget(buildTestDashboard()); await tester.pump(); expect(tester.takeException(), isNull); expect(find.byType(QuickSwarmReportButton), findsOneWidget); expect(find.byType(QuickSquadFeedButton), findsOneWidget); expect(find.byType(SpeedReadout), findsOneWidget); }); testWidgets('BUG-007 / #54: Start Session button text is fully visible without clipping in minimal landscape (map hidden)', (tester) async { await prefs.setString('dashboardLayout', 'minimal'); for (final size in [const Size(640, 360), const Size(800, 360), const Size(840, 390)]) { configureViewport(tester, size.width, size.height); await tester.pumpWidget(buildTestDashboard()); await tester.pump(); expect(tester.takeException(), isNull); final startBtnText = find.text('Start Session'); expect(startBtnText, findsOneWidget); // Verify button text has positive width and fits cleanly final textRenderBox = tester.renderObject(startBtnText) as RenderBox; expect(textRenderBox.size.width, greaterThan(0)); expect(textRenderBox.size.height, greaterThan(0)); } tester.view.resetPhysicalSize(); }); testWidgets('BUG-007 / #54: Start Session button text is fully visible in sport and touring landscape layouts', (tester) async { for (final layout in ['sport', 'touring']) { await prefs.setString('dashboardLayout', layout); for (final size in [const Size(640, 360), const Size(800, 360), const Size(2000, 900)]) { configureViewport(tester, size.width, size.height); await tester.pumpWidget(buildTestDashboard()); await tester.pump(); expect(tester.takeException(), isNull); expect(find.text('Start Session'), findsOneWidget); } } tester.view.resetPhysicalSize(); }); testWidgets('BUG-007 / #54: LEAN ANGLE title does not overlap rider graphic across landscape viewports', (tester) async { await prefs.setString('dashboardLayout', 'sport'); for (final size in [const Size(640, 360), const Size(800, 360), const Size(2000, 900)]) { configureViewport(tester, size.width, size.height); await tester.pumpWidget(buildTestDashboard()); await tester.pump(); expect(tester.takeException(), isNull); final titleFinder = find.text('LEAN ANGLE'); final graphicFinder = find.byKey(const ValueKey('lean_angle_rider_graphic')); expect(titleFinder, findsOneWidget); expect(graphicFinder, findsOneWidget); final titleRect = tester.getRect(titleFinder); final graphicRect = tester.getRect(graphicFinder); // Prove no overlap between title rect and rider graphic rect expect(titleRect.overlaps(graphicRect), isFalse, reason: 'Title overlaps rider graphic at ${size.width}x${size.height}'); // Prove title is positioned strictly above graphic expect(titleRect.bottom, lessThanOrEqualTo(graphicRect.top), reason: 'Title bottom must be above or at graphic top at ${size.width}x${size.height}'); } tester.view.resetPhysicalSize(); }); }); }