171 lines
7.1 KiB
Dart
171 lines
7.1 KiB
Dart
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<TelemetrySample> telemetryController;
|
|
late StreamController<List<RoadAlert>> alertsController;
|
|
|
|
setUp(() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
prefs = await SharedPreferences.getInstance();
|
|
telemetryController = StreamController<TelemetrySample>.broadcast();
|
|
alertsController = StreamController<List<RoadAlert>>.broadcast();
|
|
});
|
|
|
|
tearDown(() async {
|
|
await telemetryController.close();
|
|
await alertsController.close();
|
|
});
|
|
|
|
Widget buildTestDashboard({
|
|
RideSession? initialSession,
|
|
List<RoadAlert>? 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<Padding>(find.descendant(
|
|
of: find.byType(ResponsiveDashboardLayout),
|
|
matching: find.byType(Padding),
|
|
));
|
|
final edgeInsets = paddingWidget.padding as EdgeInsets;
|
|
expect(edgeInsets.right, greaterThanOrEqualTo(16.0));
|
|
});
|
|
});
|
|
}
|