MotorSignal-App/test/canonical_diagnostics_field...

343 lines
13 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:drift/native.dart';
import 'package:drift/drift.dart' as drift;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:motorcycle_dash/core/theme/theme.dart';
import 'package:motorcycle_dash/core/theme/theme_mode_provider.dart';
import 'package:motorcycle_dash/database/database.dart';
import 'package:motorcycle_dash/features/records/records_service.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/services/field_test/field_test_logger.dart';
import 'package:motorcycle_dash/services/telemetry_service.dart';
import 'package:motorcycle_dash/services/github/github_sync_service.dart';
import 'package:geolocator/geolocator.dart';
import 'package:motorcycle_dash/services/location_service.dart';
import 'package:motorcycle_dash/models/telemetry_sample.dart' as live_sample;
import 'package:motorcycle_dash/models/field_test_event.dart';
class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService {
@override
GitHubUploadState get state => GitHubUploadState.idle;
@override
String get statusMessage => '';
@override
Set<int> get queuedRideIds => const {};
@override
bool isRideUploaded(int rideId) => false;
@override
String? getRideCommitSha(int rideId) => null;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _FakeLocationService extends ChangeNotifier implements LocationService {
@override
Future<bool> requestPermissions() async => true;
@override
Future<bool> hasPermissions() async => true;
@override
void Function(Position, bool, String?)? onGpsDiagnostic;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late AppDatabase db;
late SharedPreferences prefs;
setUp(() async {
db = AppDatabase.forTesting(NativeDatabase.memory());
SharedPreferences.setMockInitialValues({});
prefs = await SharedPreferences.getInstance();
});
tearDown(() async {
await db.close();
});
final canonicalViewports = <String, Size>{
'320x640 (very narrow phone)': const Size(320, 640),
'400x800 (standard phone)': const Size(400, 800),
'412x915 (tall modern phone)': const Size(412, 915),
'640x320 (short landscape)': const Size(640, 320),
'780x360 (wide landscape)': const Size(780, 360),
'800x360 (wide landscape)': const Size(800, 360),
'840x390 (ultra-wide landscape)': const Size(840, 390),
'768x1024 (portrait tablet)': const Size(768, 1024),
'1024x768 (landscape tablet)': const Size(1024, 768),
};
Widget buildTestHost({
required Widget child,
FieldTestLogger? fieldTestLogger,
Stream<live_sample.TelemetrySample>? telemetryStream,
}) {
return ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
databaseProvider.overrideWithValue(db),
locationServiceProvider.overrideWith((ref) => _FakeLocationService()),
if (fieldTestLogger != null)
fieldTestLoggerProvider.overrideWithValue(fieldTestLogger),
githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()),
if (telemetryStream != null)
telemetryStreamProvider.overrideWith((ref) => telemetryStream),
],
child: Consumer(
builder: (context, ref, _) {
final themeMode = ref.watch(appThemeModeProvider);
return MaterialApp(
themeMode: themeMode,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
home: child,
);
},
),
);
}
group('Canonical 9-Viewport DiagnosticsScreen Responsive Tests', () {
for (final entry in canonicalViewports.entries) {
testWidgets('renders cleanly on ${entry.key}', (tester) async {
tester.view.physicalSize = entry.value;
tester.view.devicePixelRatio = 1.0;
addTearDown(() => tester.view.resetPhysicalSize());
final telemetryController = StreamController<live_sample.TelemetrySample>.broadcast();
addTearDown(() => telemetryController.close());
await tester.pumpWidget(
buildTestHost(
child: const DiagnosticsScreen(),
telemetryStream: telemetryController.stream,
),
);
await tester.pump(const Duration(milliseconds: 50));
await tester.pumpAndSettle();
expect(find.text('Sensor Diagnostics'), findsOneWidget);
expect(find.text('SENSOR HEALTH & SIGNAL'), findsOneWidget);
expect(find.text('GPS & NAVIGATION METRICS'), findsOneWidget);
expect(find.text('IMU, G-FORCE & ORIENTATION'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
});
group('Canonical 9-Viewport FieldTestLiveLogScreen Responsive Tests', () {
for (final entry in canonicalViewports.entries) {
testWidgets('renders cleanly on ${entry.key}', (tester) async {
tester.view.physicalSize = entry.value;
tester.view.devicePixelRatio = 1.0;
addTearDown(() => tester.view.resetPhysicalSize());
await tester.pumpWidget(
buildTestHost(
child: const FieldTestLiveLogScreen(),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('Black-Box Live Log'), findsOneWidget);
expect(find.text('Telemetry Rate'), findsOneWidget);
expect(find.text('GPS Accuracy'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
});
group('Canonical 9-Viewport FieldTestReportScreen Responsive Tests', () {
for (final entry in canonicalViewports.entries) {
testWidgets('renders empty state cleanly on ${entry.key}', (tester) async {
tester.view.physicalSize = entry.value;
tester.view.devicePixelRatio = 1.0;
addTearDown(() => tester.view.resetPhysicalSize());
await tester.pumpWidget(
buildTestHost(
child: const FieldTestReportScreen(),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('Forensic Field Test Report'), findsOneWidget);
expect(find.textContaining('No rides recorded yet'), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('renders populated forensic report on ${entry.key}', (tester) async {
tester.view.physicalSize = entry.value;
tester.view.devicePixelRatio = 1.0;
addTearDown(() => tester.view.resetPhysicalSize());
// Insert test ride and summary
final now = DateTime.now();
final rideId = await db.into(db.rides).insert(
RidesCompanion.insert(
startTime: now.subtract(const Duration(minutes: 10)),
distanceMiles: const drift.Value(5.2),
movingTime: const drift.Value(500),
stoppedTime: const drift.Value(100),
),
);
await db.into(db.fieldTestSummaries).insert(
FieldTestSummariesCompanion.insert(
rideId: rideId,
startTime: now.subtract(const Duration(minutes: 10)),
durationSeconds: const drift.Value(600),
totalSamples: const drift.Value(12000),
validSamples: const drift.Value(11980),
suspectSamples: const drift.Value(15),
rejectedSamples: const drift.Value(5),
validPercentage: const drift.Value(99.8),
suspectPercentage: const drift.Value(0.1),
rejectedPercentage: const drift.Value(0.1),
gpsFixCount: const drift.Value(11950),
gpsStaleEventCount: const drift.Value(2),
gpsJumpRejectCount: const drift.Value(1),
telemetryGapCount: const drift.Value(0),
imuSaturationCount: const drift.Value(0),
accelerationEventCount: const drift.Value(4),
brakingEventCount: const drift.Value(3),
zeroTo60Count: const drift.Value(1),
zeroTo100Count: const drift.Value(0),
databaseSamplesWritten: const drift.Value(12000),
databaseSamplesLost: const drift.Value(0),
eventCount: const drift.Value(10),
rideQualityScore: const drift.Value(96),
rideQualityRating: const drift.Value('EXCELLENT'),
),
);
await tester.pumpWidget(
buildTestHost(
child: FieldTestReportScreen(rideId: rideId),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('FIELD TEST RESULT'), findsOneWidget);
expect(find.text('EXCELLENT'), findsOneWidget);
expect(find.text('96'), findsOneWidget);
expect(find.text('SENSOR HEALTH & PIPELINE'), findsOneWidget);
expect(find.text('PERFORMANCE SPRINT METRICS'), findsOneWidget);
expect(find.text('FORENSIC EVENT TIMELINE'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
});
group('Canonical 9-Viewport FieldTestPreRideCheckScreen Responsive Tests', () {
for (final entry in canonicalViewports.entries) {
testWidgets('renders cleanly on ${entry.key}', (tester) async {
tester.view.physicalSize = entry.value;
tester.view.devicePixelRatio = 1.0;
addTearDown(() => tester.view.resetPhysicalSize());
await tester.pumpWidget(
buildTestHost(
child: const FieldTestPreRideCheckScreen(),
),
);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Field Test Pre-Ride Check'), findsOneWidget);
expect(find.text('REQUIRED SYSTEM CHECKS'), findsOneWidget);
expect(find.text('RECOMMENDED PRE-FLIGHT CHECKS'), findsOneWidget);
expect(find.text('SQLite Database Storage'), findsOneWidget);
expect(find.text('Drift SQLite Storage verified'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
});
group('Field Test Active Session & State Preservation Across Viewport & Theme', () {
testWidgets('preserves active logging session when resized and theme changed', (tester) async {
tester.view.physicalSize = const Size(400, 800);
tester.view.devicePixelRatio = 1.0;
addTearDown(() => tester.view.resetPhysicalSize());
final testLogger = FieldTestLogger(db);
testLogger.startSession(77);
testLogger.log(
eventType: FieldTestEventType.sampleAccepted,
severity: EventSeverity.info,
message: 'Telemetry sample 20Hz accepted',
);
final container = ProviderContainer(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
databaseProvider.overrideWithValue(db),
fieldTestLoggerProvider.overrideWithValue(testLogger),
githubSyncServiceProvider.overrideWith((ref) => _FakeGitHubSyncService()),
],
);
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: Consumer(
builder: (context, ref, _) {
final themeMode = ref.watch(appThemeModeProvider);
return MaterialApp(
themeMode: themeMode,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
home: const FieldTestLiveLogScreen(),
);
},
),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('RECORDING (Ride #77)'), findsOneWidget);
expect(find.text('Telemetry sample 20Hz accepted'), findsOneWidget);
// Resize viewport to landscape tablet
tester.view.physicalSize = const Size(1024, 768);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('RECORDING (Ride #77)'), findsOneWidget);
expect(find.text('Telemetry sample 20Hz accepted'), findsOneWidget);
// Toggle Theme Mode to Light
container.read(appThemeModeProvider.notifier).setThemeMode(ThemeMode.light);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('RECORDING (Ride #77)'), findsOneWidget);
expect(find.text('Telemetry sample 20Hz accepted'), findsOneWidget);
// Toggle Theme Mode back to Dark
container.read(appThemeModeProvider.notifier).setThemeMode(ThemeMode.dark);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('RECORDING (Ride #77)'), findsOneWidget);
expect(find.text('Telemetry sample 20Hz accepted'), findsOneWidget);
testLogger.dispose();
container.dispose();
});
});
}